@xelth/eck-snapshot 1.5.1 → 2.1.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 +86 -22
- package/index.js +155 -16
- package/package.json +4 -11
package/README.md
CHANGED
|
@@ -1,15 +1,76 @@
|
|
|
1
|
+
# eck-snapshot
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
[](https://www.google.com/search?q=https://www.npmjs.com/package/%40xelth/eck-snapshot)
|
|
4
|
+
[](https://www.google.com/search?q=https://github.com/xelth-com/eckSnapshot/blob/main/LICENSE)
|
|
3
5
|
|
|
4
|
-
A CLI tool to create
|
|
6
|
+
A CLI tool to create and restore single-file text snapshots of a Git repository. It generates a single `.txt` file containing the directory structure and the content of all text-based files, which is ideal for providing context to Large Language Models (LLMs).
|
|
5
7
|
|
|
6
|
-
##
|
|
8
|
+
## Why eck-snapshot?
|
|
7
9
|
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
When working with LLMs like Claude or GPT-4, you often need to provide the full context of your project. Manually copying and pasting dozens of files is tedious and inefficient. This tool automates the process by creating a single, comprehensive file that you can easily feed into an LLM. And with the new `restore` feature, you can instantly recreate a project structure from a snapshot.
|
|
11
|
+
|
|
12
|
+
## Key Features
|
|
13
|
+
|
|
14
|
+
* **Git Integration**: Automatically includes all files tracked by Git.
|
|
15
|
+
* **Intelligent Ignoring**: Respects `.gitignore` rules and has its own configurable ignore lists for files, extensions, and directories.
|
|
16
|
+
* **Restore from Snapshot**: The new `restore` command allows you to recreate files and folders from a snapshot file.
|
|
17
|
+
* **Directory Tree**: Generates a clean, readable tree of the repository structure at the top of the snapshot.
|
|
18
|
+
* **Configurable**: Customize behavior using an `.ecksnapshot.config.js` file.
|
|
19
|
+
* **Progress and Stats**: Provides a progress bar and a detailed summary of what was included and skipped.
|
|
20
|
+
* **Compression**: Supports gzipped (`.gz`) snapshots for smaller file sizes.
|
|
21
|
+
|
|
22
|
+
## Demo
|
|
23
|
+
|
|
24
|
+
Here's an example of `eck-snapshot` in action:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
🚀 Starting snapshot for repository: /path/to/your/project
|
|
28
|
+
✅ .gitignore patterns loaded
|
|
29
|
+
📊 Found 152 total files in the repository
|
|
30
|
+
🌳 Generating directory tree...
|
|
31
|
+
📝 Processing files...
|
|
32
|
+
Progress |██████████████████████████████| 100% | 152/152 files
|
|
33
|
+
|
|
34
|
+
📊 Snapshot Summary
|
|
35
|
+
==================================================
|
|
36
|
+
🎉 Snapshot created successfully!
|
|
37
|
+
📄 File saved to: /path/to/your/project/snapshots/project_snapshot_...txt
|
|
38
|
+
📈 Included text files: 130 of 152
|
|
39
|
+
⏭️ Skipped files: 22
|
|
40
|
+
...
|
|
41
|
+
==================================================
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The beginning of the generated file will look like this:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
Directory Structure:
|
|
48
|
+
|
|
49
|
+
├── .github/
|
|
50
|
+
│ └── workflows/
|
|
51
|
+
│ └── publish.yml
|
|
52
|
+
├── src/
|
|
53
|
+
│ ├── utils/
|
|
54
|
+
│ │ └── formatters.js
|
|
55
|
+
│ └── index.js
|
|
56
|
+
├── .gitignore
|
|
57
|
+
├── package.json
|
|
58
|
+
└── README.md
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
--- File: /src/index.js ---
|
|
62
|
+
|
|
63
|
+
#!/usr/bin/env node
|
|
64
|
+
import { Command } from 'commander';
|
|
65
|
+
// ... rest of the file content
|
|
66
|
+
|
|
67
|
+
--- File: /package.json ---
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
"name": "eck-snapshot",
|
|
71
|
+
"version": "2.1.0",
|
|
72
|
+
// ... rest of the file content
|
|
73
|
+
```
|
|
13
74
|
|
|
14
75
|
## Installation
|
|
15
76
|
|
|
@@ -17,31 +78,34 @@ To install the tool globally, run the following command:
|
|
|
17
78
|
|
|
18
79
|
```bash
|
|
19
80
|
npm install -g @xelth/eck-snapshot
|
|
20
|
-
|
|
21
|
-
|
|
81
|
+
```
|
|
22
82
|
|
|
23
83
|
## Usage
|
|
24
84
|
|
|
25
|
-
Once installed, you can run the tool from any directory in your terminal.
|
|
85
|
+
Once installed, you can run the tool from any directory in your terminal.
|
|
26
86
|
|
|
27
|
-
|
|
87
|
+
### Creating a Snapshot
|
|
28
88
|
|
|
29
89
|
```bash
|
|
90
|
+
# Create a snapshot of the current directory
|
|
30
91
|
eck-snapshot
|
|
31
|
-
```
|
|
32
92
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```bash
|
|
93
|
+
# Specify a path to another repository
|
|
36
94
|
eck-snapshot /path/to/your/other/project
|
|
95
|
+
|
|
96
|
+
# Save the snapshot to a different directory and exclude the tree view
|
|
97
|
+
eck-snapshot --output ./backups --no-tree
|
|
37
98
|
```
|
|
38
99
|
|
|
39
|
-
|
|
100
|
+
### Restoring from a Snapshot
|
|
40
101
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
102
|
+
```bash
|
|
103
|
+
# Restore files from a snapshot into the current directory
|
|
104
|
+
eck-snapshot restore ./snapshots/project_snapshot_...txt
|
|
105
|
+
|
|
106
|
+
# Restore into a new directory without a confirmation prompt
|
|
107
|
+
eck-snapshot restore snapshot.txt ./restored-project --force
|
|
108
|
+
```
|
|
45
109
|
|
|
46
110
|
## Configuration
|
|
47
111
|
|
|
@@ -74,4 +138,4 @@ export default {
|
|
|
74
138
|
|
|
75
139
|
## License
|
|
76
140
|
|
|
77
|
-
This project is licensed under the MIT License.
|
|
141
|
+
This project is licensed under the MIT License.
|
package/index.js
CHANGED
|
@@ -11,9 +11,12 @@ import { SingleBar, Presets } from 'cli-progress';
|
|
|
11
11
|
import pLimit from 'p-limit';
|
|
12
12
|
import zlib from 'zlib';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
|
+
// NEW: Import inquirer for user prompts
|
|
15
|
+
import inquirer from 'inquirer';
|
|
14
16
|
|
|
15
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
const gzip = promisify(zlib.gzip);
|
|
19
|
+
const gunzip = promisify(zlib.gunzip);
|
|
17
20
|
|
|
18
21
|
const DEFAULT_CONFIG = {
|
|
19
22
|
filesToIgnore: ['package-lock.json', '*.log', 'yarn.lock'],
|
|
@@ -25,6 +28,9 @@ const DEFAULT_CONFIG = {
|
|
|
25
28
|
concurrency: 10
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
// ... (остальные существующие функции: parseSize, formatSize, matchesPattern, loadConfig, и т.д. остаются без изменений)
|
|
32
|
+
// --- НАЧАЛО СУЩЕСТВУЮЩИХ ФУНКЦИЙ ---
|
|
33
|
+
|
|
28
34
|
function parseSize(sizeStr) {
|
|
29
35
|
const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
|
|
30
36
|
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
@@ -44,19 +50,9 @@ function formatSize(bytes) {
|
|
|
44
50
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
/**
|
|
48
|
-
* Correctly matches a file path against glob-like patterns.
|
|
49
|
-
* @param {string} filePath - The path of the file to check.
|
|
50
|
-
* @param {string[]} patterns - An array of patterns to match against.
|
|
51
|
-
* @returns {boolean} - True if the file path matches any of the patterns.
|
|
52
|
-
*/
|
|
53
53
|
function matchesPattern(filePath, patterns) {
|
|
54
54
|
const fileName = path.basename(filePath);
|
|
55
55
|
return patterns.some(pattern => {
|
|
56
|
-
// This is a robust way to convert simple globs to regex
|
|
57
|
-
// 1. Escape all special regex characters.
|
|
58
|
-
// 2. Convert the glob star '*' into the regex '.*'.
|
|
59
|
-
// 3. Anchor the pattern to match the whole string.
|
|
60
56
|
const regexPattern = '^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
|
|
61
57
|
try {
|
|
62
58
|
const regex = new RegExp(regexPattern);
|
|
@@ -253,8 +249,13 @@ async function processFile(filePath, config, gitignore, stats) {
|
|
|
253
249
|
return { skipped: true, reason: error.message };
|
|
254
250
|
}
|
|
255
251
|
}
|
|
252
|
+
// --- КОНЕЦ СУЩЕСТВУЮЩИХ ФУНКЦИЙ ---
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
// --- ОСНОВНЫЕ ФУНКЦИИ ДЛЯ КОМАНД ---
|
|
256
256
|
|
|
257
257
|
async function createRepoSnapshot(repoPath, options) {
|
|
258
|
+
// ... (эта функция остается без изменений)
|
|
258
259
|
const absoluteRepoPath = path.resolve(repoPath);
|
|
259
260
|
const absoluteOutputPath = path.resolve(options.output);
|
|
260
261
|
const originalCwd = process.cwd();
|
|
@@ -508,24 +509,162 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
508
509
|
}
|
|
509
510
|
}
|
|
510
511
|
|
|
511
|
-
//
|
|
512
|
+
// NEW: Restore Snapshot Function
|
|
513
|
+
async function restoreSnapshot(snapshotFile, targetDir, options) {
|
|
514
|
+
const absoluteSnapshotPath = path.resolve(snapshotFile);
|
|
515
|
+
const absoluteTargetDir = path.resolve(targetDir);
|
|
516
|
+
|
|
517
|
+
console.log(`⚙️ Starting restore from snapshot: ${absoluteSnapshotPath}`);
|
|
518
|
+
console.log(`🗂️ Target directory: ${absoluteTargetDir}`);
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
let rawContent;
|
|
522
|
+
// Check if the file is compressed
|
|
523
|
+
if (snapshotFile.endsWith('.gz')) {
|
|
524
|
+
const compressedBuffer = await fs.readFile(absoluteSnapshotPath);
|
|
525
|
+
rawContent = (await gunzip(compressedBuffer)).toString('utf-8');
|
|
526
|
+
console.log('✅ Decompressed gzipped snapshot.');
|
|
527
|
+
} else {
|
|
528
|
+
rawContent = await fs.readFile(absoluteSnapshotPath, 'utf-8');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Check if the content is JSON
|
|
532
|
+
let filesToRestore;
|
|
533
|
+
try {
|
|
534
|
+
const jsonData = JSON.parse(rawContent);
|
|
535
|
+
if (jsonData.content) {
|
|
536
|
+
console.log('📄 Detected JSON format, extracting content.');
|
|
537
|
+
filesToRestore = parseSnapshotContent(jsonData.content);
|
|
538
|
+
} else {
|
|
539
|
+
throw new Error('JSON format detected, but no "content" key found.');
|
|
540
|
+
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
// Not a JSON file, or not the format we expect. Treat as plain text.
|
|
543
|
+
console.log('📄 Treating snapshot as plain text format.');
|
|
544
|
+
filesToRestore = parseSnapshotContent(rawContent);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (filesToRestore.length === 0) {
|
|
548
|
+
console.warn('⚠️ No files found to restore in the snapshot.');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log(`📊 Found ${filesToRestore.length} files in the snapshot.`);
|
|
553
|
+
|
|
554
|
+
// Confirmation prompt
|
|
555
|
+
if (!options.force) {
|
|
556
|
+
const { confirm } = await inquirer.prompt([
|
|
557
|
+
{
|
|
558
|
+
type: 'confirm',
|
|
559
|
+
name: 'confirm',
|
|
560
|
+
message: `You are about to write ${filesToRestore.length} files to ${absoluteTargetDir}. Existing files will be overwritten. Are you sure you want to continue?`,
|
|
561
|
+
default: false,
|
|
562
|
+
},
|
|
563
|
+
]);
|
|
564
|
+
|
|
565
|
+
if (!confirm) {
|
|
566
|
+
console.log('🚫 Restore operation cancelled by user.');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Create target directory if it doesn't exist
|
|
572
|
+
await fs.mkdir(absoluteTargetDir, { recursive: true });
|
|
573
|
+
|
|
574
|
+
// Restore files
|
|
575
|
+
const progressBar = new SingleBar({
|
|
576
|
+
format: 'Restoring |{bar}| {percentage}% | {value}/{total} files',
|
|
577
|
+
barCompleteChar: '\u2588',
|
|
578
|
+
barIncompleteChar: '\u2591',
|
|
579
|
+
hideCursor: true
|
|
580
|
+
}, Presets.shades_classic);
|
|
581
|
+
|
|
582
|
+
progressBar.start(filesToRestore.length, 0);
|
|
583
|
+
|
|
584
|
+
for (const file of filesToRestore) {
|
|
585
|
+
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
586
|
+
const dir = path.dirname(fullPath);
|
|
587
|
+
|
|
588
|
+
// Create directory for the file
|
|
589
|
+
await fs.mkdir(dir, { recursive: true });
|
|
590
|
+
// Write the file content
|
|
591
|
+
await fs.writeFile(fullPath, file.content);
|
|
592
|
+
progressBar.increment();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
progressBar.stop();
|
|
596
|
+
console.log(`\n🎉 Restore complete! ${filesToRestore.length} files have been written to ${absoluteTargetDir}.`);
|
|
597
|
+
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error('\n❌ An error occurred during restore:');
|
|
600
|
+
console.error(error.message);
|
|
601
|
+
if (options.verbose) {
|
|
602
|
+
console.error(error.stack);
|
|
603
|
+
}
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// NEW: Snapshot Parser Function
|
|
609
|
+
function parseSnapshotContent(content) {
|
|
610
|
+
const files = [];
|
|
611
|
+
const fileRegex = /--- File: \/(.+) ---/g;
|
|
612
|
+
const sections = content.split(fileRegex);
|
|
613
|
+
|
|
614
|
+
// sections will be [ '...everything before first match...', 'path1', 'content1', 'path2', 'content2', ...]
|
|
615
|
+
// We start at index 1 because index 0 is anything before the first delimiter (like the directory tree)
|
|
616
|
+
for (let i = 1; i < sections.length; i += 2) {
|
|
617
|
+
const filePath = sections[i].trim();
|
|
618
|
+
let fileContent = sections[i + 1] || '';
|
|
619
|
+
|
|
620
|
+
// Remove the trailing newline that the split might leave
|
|
621
|
+
if (fileContent.startsWith('\n\n')) {
|
|
622
|
+
fileContent = fileContent.substring(2);
|
|
623
|
+
}
|
|
624
|
+
if (fileContent.endsWith('\n\n')) {
|
|
625
|
+
fileContent = fileContent.substring(0, fileContent.length - 2);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
files.push({ path: filePath, content: fileContent });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return files;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
// --- CLI SETUP ---
|
|
512
636
|
const program = new Command();
|
|
513
637
|
|
|
514
638
|
program
|
|
515
639
|
.name('eck-snapshot')
|
|
516
|
-
.description('A CLI tool to create
|
|
517
|
-
.version('2.
|
|
640
|
+
.description('A CLI tool to create and restore single-file text snapshots of a Git repository.')
|
|
641
|
+
.version('2.1.0');
|
|
642
|
+
|
|
643
|
+
// Snapshot command (existing)
|
|
644
|
+
program
|
|
645
|
+
.command('snapshot', { isDefault: true })
|
|
646
|
+
.description('Create a snapshot of a Git repository (default command).')
|
|
518
647
|
.argument('[repoPath]', 'Path to the git repository to snapshot.', process.cwd())
|
|
519
|
-
.option('-o, --output <dir>', 'Output directory for the snapshot file.', path.join(
|
|
648
|
+
.option('-o, --output <dir>', 'Output directory for the snapshot file.', path.join(process.cwd(), 'snapshots'))
|
|
520
649
|
.option('--no-tree', 'Do not include the directory tree in the snapshot.')
|
|
521
650
|
.option('-v, --verbose', 'Show detailed processing information, including skipped files.')
|
|
522
651
|
.option('--max-file-size <size>', 'Maximum file size to include (e.g., 10MB)', '10MB')
|
|
523
652
|
.option('--max-total-size <size>', 'Maximum total snapshot size (e.g., 100MB)', '100MB')
|
|
524
|
-
.option('--max-depth <number>', 'Maximum directory depth for tree generation', parseInt, 10)
|
|
653
|
+
.option('--max-depth <number>', 'Maximum directory depth for tree generation', (val) => parseInt(val), 10)
|
|
525
654
|
.option('--config <path>', 'Path to configuration file')
|
|
526
655
|
.option('--compress', 'Compress output file with gzip')
|
|
527
656
|
.option('--include-hidden', 'Include hidden files (starting with .)')
|
|
528
657
|
.option('--format <type>', 'Output format: txt, json', 'txt')
|
|
529
|
-
.action(createRepoSnapshot);
|
|
658
|
+
.action((repoPath, options) => createRepoSnapshot(repoPath, options));
|
|
659
|
+
|
|
660
|
+
// NEW: Restore command (Corrected)
|
|
661
|
+
program
|
|
662
|
+
.command('restore') // Убираем аргументы отсюда
|
|
663
|
+
.description('Restore files and directories from a snapshot file.')
|
|
664
|
+
.argument('<snapshot_file>', 'Path to the snapshot file (.txt, .json, or .gz).') // Аргументы определяем здесь
|
|
665
|
+
.argument('[target_directory]', 'Directory to restore the files into.', process.cwd())
|
|
666
|
+
.option('-f, --force', 'Force overwrite of existing files without confirmation.')
|
|
667
|
+
.option('-v, --verbose', 'Show detailed processing information.')
|
|
668
|
+
.action((snapshotFile, targetDir, options) => restoreSnapshot(snapshotFile, targetDir, options));
|
|
530
669
|
|
|
531
670
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xelth/eck-snapshot",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A CLI tool to create
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "A CLI tool to create and restore single-file text snapshots of a Git repository.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -16,14 +16,6 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
18
|
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"snapshot",
|
|
21
|
-
"repository",
|
|
22
|
-
"cli",
|
|
23
|
-
"git",
|
|
24
|
-
"llm",
|
|
25
|
-
"context"
|
|
26
|
-
],
|
|
27
19
|
"author": "xelth-com",
|
|
28
20
|
"license": "MIT",
|
|
29
21
|
"repository": {
|
|
@@ -36,6 +28,7 @@
|
|
|
36
28
|
"is-binary-path": "^2.1.0",
|
|
37
29
|
"ignore": "^5.3.1",
|
|
38
30
|
"cli-progress": "^3.12.0",
|
|
39
|
-
"p-limit": "^5.0.0"
|
|
31
|
+
"p-limit": "^5.0.0",
|
|
32
|
+
"inquirer": "^9.2.20"
|
|
40
33
|
}
|
|
41
34
|
}
|