@xelth/eck-snapshot 1.5.1 → 2.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/README.md +171 -22
- package/index.js +261 -16
- package/package.json +5 -12
package/README.md
CHANGED
|
@@ -1,15 +1,80 @@
|
|
|
1
|
+
# eck-snapshot
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@xelth/eck-snapshot)
|
|
4
|
+
[](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 Large Language Models (LLMs), providing the full context of your project is crucial for getting accurate results. Manually copying and pasting dozens of files is tedious and inefficient.
|
|
11
|
+
|
|
12
|
+
eck-snapshot automates this by generating a single, comprehensive text file of your entire repository. This is particularly effective with models that support large context windows (like Google's Gemini), as it often allows the entire project snapshot to be analyzed at once—a task that can be challenging with smaller context windows.
|
|
13
|
+
|
|
14
|
+
## Key Features
|
|
15
|
+
|
|
16
|
+
* **Git Integration**: Automatically includes all files tracked by Git.
|
|
17
|
+
* **Intelligent Ignoring**: Respects `.gitignore` rules and has its own configurable ignore lists for files, extensions, and directories.
|
|
18
|
+
* **Advanced Restore**: Powerful `restore` command with filtering, dry-run mode, and parallel processing.
|
|
19
|
+
* **Directory Tree**: Generates a clean, readable tree of the repository structure at the top of the snapshot.
|
|
20
|
+
* **Multiple Formats**: Supports both plain text and JSON output formats.
|
|
21
|
+
* **Configurable**: Customize behavior using an `.ecksnapshot.config.js` file.
|
|
22
|
+
* **Progress and Stats**: Provides a progress bar and a detailed summary of what was included and skipped.
|
|
23
|
+
* **Compression**: Supports gzipped (`.gz`) snapshots for smaller file sizes.
|
|
24
|
+
* **Security**: Built-in path validation to prevent directory traversal attacks during restore.
|
|
25
|
+
|
|
26
|
+
## Demo
|
|
27
|
+
|
|
28
|
+
Here's an example of `eck-snapshot` in action:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
🚀 Starting snapshot for repository: /path/to/your/project
|
|
32
|
+
✅ .gitignore patterns loaded
|
|
33
|
+
📊 Found 152 total files in the repository
|
|
34
|
+
🌳 Generating directory tree...
|
|
35
|
+
📝 Processing files...
|
|
36
|
+
Progress |██████████████████████████████| 100% | 152/152 files
|
|
37
|
+
|
|
38
|
+
📊 Snapshot Summary
|
|
39
|
+
==================================================
|
|
40
|
+
🎉 Snapshot created successfully!
|
|
41
|
+
📄 File saved to: /path/to/your/project/snapshots/project_snapshot_...txt
|
|
42
|
+
📈 Included text files: 130 of 152
|
|
43
|
+
⏭️ Skipped files: 22
|
|
44
|
+
...
|
|
45
|
+
==================================================
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The beginning of the generated file will look like this:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Directory Structure:
|
|
52
|
+
|
|
53
|
+
├── .github/
|
|
54
|
+
│ └── workflows/
|
|
55
|
+
│ └── publish.yml
|
|
56
|
+
├── src/
|
|
57
|
+
│ ├── utils/
|
|
58
|
+
│ │ └── formatters.js
|
|
59
|
+
│ └── index.js
|
|
60
|
+
├── .gitignore
|
|
61
|
+
├── package.json
|
|
62
|
+
└── README.md
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
--- File: /src/index.js ---
|
|
66
|
+
|
|
67
|
+
#!/usr/bin/env node
|
|
68
|
+
import { Command } from 'commander';
|
|
69
|
+
// ... rest of the file content
|
|
70
|
+
|
|
71
|
+
--- File: /package.json ---
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
"name": "eck-snapshot",
|
|
75
|
+
"version": "2.1.0",
|
|
76
|
+
// ... rest of the file content
|
|
77
|
+
```
|
|
13
78
|
|
|
14
79
|
## Installation
|
|
15
80
|
|
|
@@ -17,31 +82,55 @@ To install the tool globally, run the following command:
|
|
|
17
82
|
|
|
18
83
|
```bash
|
|
19
84
|
npm install -g @xelth/eck-snapshot
|
|
20
|
-
|
|
21
|
-
|
|
85
|
+
```
|
|
22
86
|
|
|
23
87
|
## Usage
|
|
24
88
|
|
|
25
|
-
Once installed, you can run the tool from any directory in your terminal.
|
|
89
|
+
Once installed, you can run the tool from any directory in your terminal.
|
|
26
90
|
|
|
27
|
-
|
|
91
|
+
### Creating a Snapshot
|
|
28
92
|
|
|
29
93
|
```bash
|
|
94
|
+
# Create a snapshot of the current directory
|
|
30
95
|
eck-snapshot
|
|
96
|
+
|
|
97
|
+
# Specify a path to another repository
|
|
98
|
+
eck-snapshot /path/to/your/other/project
|
|
99
|
+
|
|
100
|
+
# Save the snapshot to a different directory and exclude the tree view
|
|
101
|
+
eck-snapshot --output ./backups --no-tree
|
|
102
|
+
|
|
103
|
+
# Create a compressed JSON snapshot
|
|
104
|
+
eck-snapshot --format json --compress
|
|
105
|
+
|
|
106
|
+
# Include hidden files and set custom size limits
|
|
107
|
+
eck-snapshot --include-hidden --max-file-size 5MB --max-total-size 50MB
|
|
31
108
|
```
|
|
32
109
|
|
|
33
|
-
|
|
110
|
+
### Restoring from a Snapshot
|
|
34
111
|
|
|
35
112
|
```bash
|
|
36
|
-
|
|
37
|
-
|
|
113
|
+
# Basic restore to current directory
|
|
114
|
+
eck-snapshot restore ./snapshots/project_snapshot_...txt
|
|
115
|
+
|
|
116
|
+
# Restore to a specific directory without confirmation
|
|
117
|
+
eck-snapshot restore snapshot.txt ./restored-project --force
|
|
118
|
+
|
|
119
|
+
# Preview what would be restored (dry run)
|
|
120
|
+
eck-snapshot restore snapshot.txt --dry-run
|
|
121
|
+
|
|
122
|
+
# Restore only specific files using patterns
|
|
123
|
+
eck-snapshot restore snapshot.txt --include "*.js" "*.json"
|
|
124
|
+
|
|
125
|
+
# Restore everything except certain files
|
|
126
|
+
eck-snapshot restore snapshot.txt --exclude "*.log" "node_modules/*"
|
|
38
127
|
|
|
39
|
-
|
|
128
|
+
# Restore with custom concurrency and verbose output
|
|
129
|
+
eck-snapshot restore snapshot.txt --concurrency 20 --verbose
|
|
40
130
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `--config <path>`: Use a configuration file from a specific path.
|
|
131
|
+
# Restore compressed snapshots
|
|
132
|
+
eck-snapshot restore project_snapshot.txt.gz ./restored
|
|
133
|
+
```
|
|
45
134
|
|
|
46
135
|
## Configuration
|
|
47
136
|
|
|
@@ -67,11 +156,71 @@ export default {
|
|
|
67
156
|
'.git/',
|
|
68
157
|
'dist/',
|
|
69
158
|
],
|
|
70
|
-
//
|
|
159
|
+
// Size and performance settings
|
|
71
160
|
maxFileSize: '10MB',
|
|
161
|
+
maxTotalSize: '100MB',
|
|
162
|
+
maxDepth: 10,
|
|
163
|
+
concurrency: 10
|
|
72
164
|
};
|
|
73
165
|
```
|
|
74
166
|
|
|
167
|
+
## Advanced Features
|
|
168
|
+
|
|
169
|
+
### Restore Command Options
|
|
170
|
+
|
|
171
|
+
The restore command offers powerful filtering and control options:
|
|
172
|
+
|
|
173
|
+
- **`--dry-run`**: Preview what files would be restored without actually writing them
|
|
174
|
+
- **`--include <patterns>`**: Only restore files matching the specified patterns (supports wildcards)
|
|
175
|
+
- **`--exclude <patterns>`**: Skip files matching the specified patterns (supports wildcards)
|
|
176
|
+
- **`--concurrency <number>`**: Control how many files are processed simultaneously (default: 10)
|
|
177
|
+
- **`--force`**: Skip confirmation prompts and overwrite existing files
|
|
178
|
+
- **`--verbose`**: Show detailed information about each file being processed
|
|
179
|
+
|
|
180
|
+
### Supported Formats
|
|
181
|
+
|
|
182
|
+
- **Plain Text** (`.txt`): Human-readable format, ideal for LLM context
|
|
183
|
+
- **JSON** (`.json`): Structured format with metadata and statistics
|
|
184
|
+
- **Compressed** (`.gz`): Any format can be gzipped for smaller file sizes
|
|
185
|
+
|
|
186
|
+
### Security Features
|
|
187
|
+
|
|
188
|
+
- **Path Validation**: Prevents directory traversal attacks during restore operations
|
|
189
|
+
- **File Sanitization**: Validates file paths and names for security
|
|
190
|
+
- **Confirmation Prompts**: Requires user confirmation before overwriting files (unless `--force` is used)
|
|
191
|
+
|
|
192
|
+
## Command Reference
|
|
193
|
+
|
|
194
|
+
### Snapshot Command
|
|
195
|
+
```bash
|
|
196
|
+
eck-snapshot [options] [repoPath]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Options:**
|
|
200
|
+
- `-o, --output <dir>`: Output directory for snapshots
|
|
201
|
+
- `--no-tree`: Exclude directory tree from output
|
|
202
|
+
- `-v, --verbose`: Show detailed processing information
|
|
203
|
+
- `--max-file-size <size>`: Maximum individual file size (e.g., 10MB)
|
|
204
|
+
- `--max-total-size <size>`: Maximum total snapshot size (e.g., 100MB)
|
|
205
|
+
- `--max-depth <number>`: Maximum directory depth for tree generation
|
|
206
|
+
- `--config <path>`: Path to custom configuration file
|
|
207
|
+
- `--compress`: Create gzipped output
|
|
208
|
+
- `--include-hidden`: Include hidden files (starting with .)
|
|
209
|
+
- `--format <type>`: Output format: txt or json
|
|
210
|
+
|
|
211
|
+
### Restore Command
|
|
212
|
+
```bash
|
|
213
|
+
eck-snapshot restore [options] <snapshot_file> [target_directory]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Options:**
|
|
217
|
+
- `-f, --force`: Force overwrite without confirmation
|
|
218
|
+
- `-v, --verbose`: Show detailed processing information
|
|
219
|
+
- `--dry-run`: Preview without actually writing files
|
|
220
|
+
- `--include <patterns>`: Include only matching files
|
|
221
|
+
- `--exclude <patterns>`: Exclude matching files
|
|
222
|
+
- `--concurrency <number>`: Number of concurrent operations
|
|
223
|
+
|
|
75
224
|
## License
|
|
76
225
|
|
|
77
|
-
This project is licensed under the MIT License.
|
|
226
|
+
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,268 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
508
509
|
}
|
|
509
510
|
}
|
|
510
511
|
|
|
511
|
-
|
|
512
|
+
async function restoreSnapshot(snapshotFile, targetDir, options) {
|
|
513
|
+
const absoluteSnapshotPath = path.resolve(snapshotFile);
|
|
514
|
+
const absoluteTargetDir = path.resolve(targetDir);
|
|
515
|
+
|
|
516
|
+
console.log(`🔄 Starting restore from snapshot: ${absoluteSnapshotPath}`);
|
|
517
|
+
console.log(`📁 Target directory: ${absoluteTargetDir}`);
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
let rawContent;
|
|
521
|
+
if (snapshotFile.endsWith('.gz')) {
|
|
522
|
+
const compressedBuffer = await fs.readFile(absoluteSnapshotPath);
|
|
523
|
+
rawContent = (await gunzip(compressedBuffer)).toString('utf-8');
|
|
524
|
+
console.log('✅ Decompressed gzipped snapshot');
|
|
525
|
+
} else {
|
|
526
|
+
rawContent = await fs.readFile(absoluteSnapshotPath, 'utf-8');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let filesToRestore;
|
|
530
|
+
try {
|
|
531
|
+
const jsonData = JSON.parse(rawContent);
|
|
532
|
+
if (jsonData.content) {
|
|
533
|
+
console.log('📄 Detected JSON format, extracting content');
|
|
534
|
+
filesToRestore = parseSnapshotContent(jsonData.content);
|
|
535
|
+
} else {
|
|
536
|
+
throw new Error('JSON format detected, but no "content" key found');
|
|
537
|
+
}
|
|
538
|
+
} catch (e) {
|
|
539
|
+
console.log('📄 Treating snapshot as plain text format');
|
|
540
|
+
filesToRestore = parseSnapshotContent(rawContent);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (filesToRestore.length === 0) {
|
|
544
|
+
console.warn('⚠️ No files found to restore in the snapshot');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Apply filters if specified
|
|
549
|
+
if (options.include || options.exclude) {
|
|
550
|
+
filesToRestore = filterFilesToRestore(filesToRestore, options);
|
|
551
|
+
if (filesToRestore.length === 0) {
|
|
552
|
+
console.warn('⚠️ No files remaining after applying filters');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Validate file paths for security
|
|
558
|
+
const invalidFiles = validateFilePaths(filesToRestore, absoluteTargetDir);
|
|
559
|
+
if (invalidFiles.length > 0) {
|
|
560
|
+
console.error('❌ Invalid file paths detected (potential directory traversal):');
|
|
561
|
+
invalidFiles.forEach(file => console.error(` ${file}`));
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log(`📊 Found ${filesToRestore.length} files to restore`);
|
|
566
|
+
|
|
567
|
+
if (options.dryRun) {
|
|
568
|
+
console.log('\n🔍 Dry run mode - files that would be restored:');
|
|
569
|
+
filesToRestore.forEach(file => {
|
|
570
|
+
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
571
|
+
console.log(` ${fullPath}`);
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!options.force) {
|
|
577
|
+
const { confirm } = await inquirer.prompt([{
|
|
578
|
+
type: 'confirm',
|
|
579
|
+
name: 'confirm',
|
|
580
|
+
message: `You are about to write ${filesToRestore.length} files to ${absoluteTargetDir}. Existing files will be overwritten. Continue?`,
|
|
581
|
+
default: false
|
|
582
|
+
}]);
|
|
583
|
+
|
|
584
|
+
if (!confirm) {
|
|
585
|
+
console.log('🚫 Restore operation cancelled by user');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
await fs.mkdir(absoluteTargetDir, { recursive: true });
|
|
591
|
+
|
|
592
|
+
const stats = {
|
|
593
|
+
totalFiles: filesToRestore.length,
|
|
594
|
+
restoredFiles: 0,
|
|
595
|
+
failedFiles: 0,
|
|
596
|
+
errors: []
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const progressBar = options.verbose ? null : new SingleBar({
|
|
600
|
+
format: 'Restoring |{bar}| {percentage}% | {value}/{total} files',
|
|
601
|
+
barCompleteChar: '\u2588',
|
|
602
|
+
barIncompleteChar: '\u2591',
|
|
603
|
+
hideCursor: true
|
|
604
|
+
}, Presets.shades_classic);
|
|
605
|
+
|
|
606
|
+
if (progressBar) progressBar.start(filesToRestore.length, 0);
|
|
607
|
+
|
|
608
|
+
const limit = pLimit(options.concurrency || 10);
|
|
609
|
+
const filePromises = filesToRestore.map((file, index) =>
|
|
610
|
+
limit(async () => {
|
|
611
|
+
try {
|
|
612
|
+
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
613
|
+
const dir = path.dirname(fullPath);
|
|
614
|
+
|
|
615
|
+
await fs.mkdir(dir, { recursive: true });
|
|
616
|
+
await fs.writeFile(fullPath, file.content, 'utf-8');
|
|
617
|
+
|
|
618
|
+
stats.restoredFiles++;
|
|
619
|
+
|
|
620
|
+
if (progressBar) {
|
|
621
|
+
progressBar.update(index + 1);
|
|
622
|
+
} else if (options.verbose) {
|
|
623
|
+
console.log(`✅ Restored: ${file.path}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { success: true, file: file.path };
|
|
627
|
+
} catch (error) {
|
|
628
|
+
stats.failedFiles++;
|
|
629
|
+
stats.errors.push({ file: file.path, error: error.message });
|
|
630
|
+
|
|
631
|
+
if (options.verbose) {
|
|
632
|
+
console.log(`❌ Failed to restore: ${file.path} - ${error.message}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { success: false, file: file.path, error: error.message };
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
await Promise.allSettled(filePromises);
|
|
641
|
+
if (progressBar) progressBar.stop();
|
|
642
|
+
|
|
643
|
+
console.log('\n📊 Restore Summary');
|
|
644
|
+
console.log('='.repeat(50));
|
|
645
|
+
console.log(`🎉 Restore completed!`);
|
|
646
|
+
console.log(`✅ Successfully restored: ${stats.restoredFiles} files`);
|
|
647
|
+
if (stats.failedFiles > 0) {
|
|
648
|
+
console.log(`❌ Failed to restore: ${stats.failedFiles} files`);
|
|
649
|
+
if (stats.errors.length > 0) {
|
|
650
|
+
console.log('\n⚠️ Errors encountered:');
|
|
651
|
+
stats.errors.slice(0, 5).forEach(({ file, error }) => {
|
|
652
|
+
console.log(` ${file}: ${error}`);
|
|
653
|
+
});
|
|
654
|
+
if (stats.errors.length > 5) {
|
|
655
|
+
console.log(` ... and ${stats.errors.length - 5} more errors`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
console.log(`📁 Target directory: ${absoluteTargetDir}`);
|
|
660
|
+
console.log('='.repeat(50));
|
|
661
|
+
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.error('\n❌ An error occurred during restore:');
|
|
664
|
+
console.error(error.message);
|
|
665
|
+
if (options.verbose) {
|
|
666
|
+
console.error(error.stack);
|
|
667
|
+
}
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function parseSnapshotContent(content) {
|
|
673
|
+
const files = [];
|
|
674
|
+
const fileRegex = /--- File: \/(.+) ---/g;
|
|
675
|
+
const sections = content.split(fileRegex);
|
|
676
|
+
|
|
677
|
+
for (let i = 1; i < sections.length; i += 2) {
|
|
678
|
+
const filePath = sections[i].trim();
|
|
679
|
+
let fileContent = sections[i + 1] || '';
|
|
680
|
+
|
|
681
|
+
if (fileContent.startsWith('\n\n')) {
|
|
682
|
+
fileContent = fileContent.substring(2);
|
|
683
|
+
}
|
|
684
|
+
if (fileContent.endsWith('\n\n')) {
|
|
685
|
+
fileContent = fileContent.substring(0, fileContent.length - 2);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
files.push({ path: filePath, content: fileContent });
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return files;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function filterFilesToRestore(files, options) {
|
|
695
|
+
let filtered = files;
|
|
696
|
+
|
|
697
|
+
if (options.include) {
|
|
698
|
+
const includePatterns = Array.isArray(options.include) ? options.include : [options.include];
|
|
699
|
+
filtered = filtered.filter(file =>
|
|
700
|
+
includePatterns.some(pattern => {
|
|
701
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
702
|
+
return regex.test(file.path);
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (options.exclude) {
|
|
708
|
+
const excludePatterns = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
|
|
709
|
+
filtered = filtered.filter(file =>
|
|
710
|
+
!excludePatterns.some(pattern => {
|
|
711
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
712
|
+
return regex.test(file.path);
|
|
713
|
+
})
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return filtered;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function validateFilePaths(files, targetDir) {
|
|
721
|
+
const invalidFiles = [];
|
|
722
|
+
|
|
723
|
+
for (const file of files) {
|
|
724
|
+
const normalizedPath = path.normalize(file.path);
|
|
725
|
+
|
|
726
|
+
if (normalizedPath.includes('..') ||
|
|
727
|
+
normalizedPath.startsWith('/') ||
|
|
728
|
+
normalizedPath.includes('\0') ||
|
|
729
|
+
/[<>:"|?*]/.test(normalizedPath)) {
|
|
730
|
+
invalidFiles.push(file.path);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return invalidFiles;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
// --- CLI SETUP ---
|
|
512
739
|
const program = new Command();
|
|
513
740
|
|
|
514
741
|
program
|
|
515
742
|
.name('eck-snapshot')
|
|
516
|
-
.description('A CLI tool to create
|
|
517
|
-
.version('2.
|
|
743
|
+
.description('A CLI tool to create and restore single-file text snapshots of a Git repository.')
|
|
744
|
+
.version('2.1.0');
|
|
745
|
+
|
|
746
|
+
// Snapshot command (existing)
|
|
747
|
+
program
|
|
748
|
+
.command('snapshot', { isDefault: true })
|
|
749
|
+
.description('Create a snapshot of a Git repository (default command).')
|
|
518
750
|
.argument('[repoPath]', 'Path to the git repository to snapshot.', process.cwd())
|
|
519
|
-
.option('-o, --output <dir>', 'Output directory for the snapshot file.', path.join(
|
|
751
|
+
.option('-o, --output <dir>', 'Output directory for the snapshot file.', path.join(process.cwd(), 'snapshots'))
|
|
520
752
|
.option('--no-tree', 'Do not include the directory tree in the snapshot.')
|
|
521
753
|
.option('-v, --verbose', 'Show detailed processing information, including skipped files.')
|
|
522
754
|
.option('--max-file-size <size>', 'Maximum file size to include (e.g., 10MB)', '10MB')
|
|
523
755
|
.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)
|
|
756
|
+
.option('--max-depth <number>', 'Maximum directory depth for tree generation', (val) => parseInt(val), 10)
|
|
525
757
|
.option('--config <path>', 'Path to configuration file')
|
|
526
758
|
.option('--compress', 'Compress output file with gzip')
|
|
527
759
|
.option('--include-hidden', 'Include hidden files (starting with .)')
|
|
528
760
|
.option('--format <type>', 'Output format: txt, json', 'txt')
|
|
529
|
-
.action(createRepoSnapshot);
|
|
761
|
+
.action((repoPath, options) => createRepoSnapshot(repoPath, options));
|
|
762
|
+
|
|
763
|
+
program
|
|
764
|
+
.command('restore')
|
|
765
|
+
.description('Restore files and directories from a snapshot file')
|
|
766
|
+
.argument('<snapshot_file>', 'Path to the snapshot file (.txt, .json, or .gz)')
|
|
767
|
+
.argument('[target_directory]', 'Directory to restore the files into', process.cwd())
|
|
768
|
+
.option('-f, --force', 'Force overwrite of existing files without confirmation')
|
|
769
|
+
.option('-v, --verbose', 'Show detailed processing information')
|
|
770
|
+
.option('--dry-run', 'Show what would be restored without actually writing files')
|
|
771
|
+
.option('--include <patterns...>', 'Include only files matching these patterns (supports wildcards)')
|
|
772
|
+
.option('--exclude <patterns...>', 'Exclude files matching these patterns (supports wildcards)')
|
|
773
|
+
.option('--concurrency <number>', 'Number of concurrent file operations', (val) => parseInt(val), 10)
|
|
774
|
+
.action((snapshotFile, targetDir, options) => restoreSnapshot(snapshotFile, targetDir, options));
|
|
530
775
|
|
|
531
776
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xelth/eck-snapshot",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A CLI tool to create
|
|
3
|
+
"version": "2.2.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
|
+
}
|