@xelth/eck-snapshot 2.1.0 → 3.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.
- package/LICENSE +21 -0
- package/README.md +231 -80
- package/index.js +413 -112
- package/package.json +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dmytro Surovtsev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,117 +1,126 @@
|
|
|
1
1
|
# eck-snapshot
|
|
2
2
|
|
|
3
|
-
[](https://
|
|
4
|
-
[](https://
|
|
3
|
+
[](https://www.npmjs.com/package/@xelth/eck-snapshot)
|
|
4
|
+
[](https://github.com/xelth-com/eckSnapshot/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
A CLI tool to create and restore single-file text snapshots of
|
|
6
|
+
A powerful CLI tool to create and restore single-file text snapshots of Git repositories and directories. Generate comprehensive snapshots containing directory structure and file contents, optimized for providing context to Large Language Models (LLMs) like Claude, Gemini, and ChatGPT.
|
|
7
|
+
|
|
8
|
+
## ✨ What's New in v3.0.0
|
|
9
|
+
|
|
10
|
+
🎯 **Universal Directory Support**: Works with any directory, not just Git repositories
|
|
11
|
+
🤖 **Enhanced AI Instructions**: Improved headers with detailed guidance for AI assistants
|
|
12
|
+
⚡ **Auto-Detection**: Automatically switches to directory mode when Git isn't available
|
|
13
|
+
🧹 **Clean Mode**: Option to create snapshots without AI instructions
|
|
7
14
|
|
|
8
15
|
## Why eck-snapshot?
|
|
9
16
|
|
|
10
|
-
When working with
|
|
17
|
+
When working with Large Language Models (LLMs), providing complete project context is crucial for accurate results. Manually copying and pasting dozens of files is tedious and error-prone.
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
eck-snapshot automates this by generating a single, comprehensive text file of your entire project. This is particularly effective with models that support large context windows (like Gemini 2.0 Pro with 1M tokens), allowing the entire project to be analyzed at once.
|
|
13
20
|
|
|
14
|
-
|
|
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
|
+
## 🚀 Key Features
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
### 📁 **Universal Compatibility**
|
|
24
|
+
- **Git Repositories**: Leverages `git ls-files` and respects `.gitignore`
|
|
25
|
+
- **Any Directory**: Recursively scans any folder structure
|
|
26
|
+
- **Auto-Detection**: Automatically switches modes based on Git availability
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
### 🤖 **AI-Optimized**
|
|
29
|
+
- **Structured Headers**: Detailed instructions for AI assistants
|
|
30
|
+
- **Clean Mode**: Option to skip AI headers for general use
|
|
31
|
+
- **LLM-Ready Format**: Optimized for Claude, Gemini, ChatGPT, and other models
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
```
|
|
33
|
+
### ⚡ **Advanced Features**
|
|
34
|
+
- **Multiple Formats**: Plain text (Markdown) and JSON output
|
|
35
|
+
- **Compression**: Built-in gzip support for smaller files
|
|
36
|
+
- **Smart Filtering**: Configurable ignore patterns and size limits
|
|
37
|
+
- **Restore Capability**: Recreate entire project structures from snapshots
|
|
38
|
+
- **Progress Tracking**: Real-time progress bars and detailed statistics
|
|
39
|
+
|
|
40
|
+
### 🔒 **Security & Performance**
|
|
41
|
+
- **Path Validation**: Prevents directory traversal attacks
|
|
42
|
+
- **Parallel Processing**: Concurrent file handling for speed
|
|
43
|
+
- **Memory Efficient**: Handles large projects without memory issues
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
## 📦 Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g @xelth/eck-snapshot
|
|
49
|
+
```
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
Directory Structure:
|
|
51
|
+
## 🎯 Quick Start
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
│ └── workflows/
|
|
51
|
-
│ └── publish.yml
|
|
52
|
-
├── src/
|
|
53
|
-
│ ├── utils/
|
|
54
|
-
│ │ └── formatters.js
|
|
55
|
-
│ └── index.js
|
|
56
|
-
├── .gitignore
|
|
57
|
-
├── package.json
|
|
58
|
-
└── README.md
|
|
53
|
+
### Create Snapshots
|
|
59
54
|
|
|
55
|
+
```bash
|
|
56
|
+
# Git repository (default mode)
|
|
57
|
+
eck-snapshot
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
# Any directory (auto-detects non-git folders)
|
|
60
|
+
eck-snapshot /path/to/any/folder
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// ... rest of the file content
|
|
62
|
+
# Force directory mode (ignores git)
|
|
63
|
+
eck-snapshot --dir .
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
# Clean snapshot without AI instructions
|
|
66
|
+
eck-snapshot --no-ai-header
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"version": "2.1.0",
|
|
72
|
-
// ... rest of the file content
|
|
68
|
+
# Compressed JSON format
|
|
69
|
+
eck-snapshot --format json --compress
|
|
73
70
|
```
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
To install the tool globally, run the following command:
|
|
72
|
+
### Restore from Snapshots
|
|
78
73
|
|
|
79
74
|
```bash
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
# Basic restore
|
|
76
|
+
eck-snapshot restore snapshot.md
|
|
82
77
|
|
|
83
|
-
|
|
78
|
+
# Restore to specific directory
|
|
79
|
+
eck-snapshot restore snapshot.md ./restored-project
|
|
84
80
|
|
|
85
|
-
|
|
81
|
+
# Preview without writing files
|
|
82
|
+
eck-snapshot restore snapshot.md --dry-run
|
|
86
83
|
|
|
87
|
-
|
|
84
|
+
# Restore only specific files
|
|
85
|
+
eck-snapshot restore snapshot.md --include "*.js" "*.json"
|
|
86
|
+
```
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
## 📋 Usage Examples
|
|
89
|
+
|
|
90
|
+
### For AI Development
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
```bash
|
|
93
|
+
# Create AI-optimized snapshot for Gemini/Claude
|
|
94
|
+
eck-snapshot --format md --compress
|
|
95
|
+
# Result: project_snapshot_2025-01-19_12-00-00.md.gz
|
|
95
96
|
|
|
96
|
-
#
|
|
97
|
-
eck-snapshot --output ./
|
|
97
|
+
# Clean snapshot for general documentation
|
|
98
|
+
eck-snapshot --no-ai-header --output ./docs
|
|
98
99
|
```
|
|
99
100
|
|
|
100
|
-
###
|
|
101
|
+
### Project Backup & Migration
|
|
101
102
|
|
|
102
103
|
```bash
|
|
103
|
-
#
|
|
104
|
-
eck-snapshot
|
|
104
|
+
# Full project backup
|
|
105
|
+
eck-snapshot --include-hidden --format json --compress
|
|
105
106
|
|
|
106
|
-
#
|
|
107
|
-
eck-snapshot restore
|
|
107
|
+
# Selective restore
|
|
108
|
+
eck-snapshot restore backup.json.gz --exclude "node_modules/*" --include "src/*"
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
### Cross-Platform Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Create snapshot on Windows
|
|
115
|
+
eck-snapshot --output ./transfer
|
|
116
|
+
|
|
117
|
+
# Restore on Linux/Mac
|
|
118
|
+
eck-snapshot restore transfer/project_snapshot.md ./project
|
|
119
|
+
```
|
|
111
120
|
|
|
112
|
-
|
|
121
|
+
## ⚙️ Configuration
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
Create `.ecksnapshot.config.js` in your project root:
|
|
115
124
|
|
|
116
125
|
```javascript
|
|
117
126
|
export default {
|
|
@@ -119,23 +128,165 @@ export default {
|
|
|
119
128
|
filesToIgnore: [
|
|
120
129
|
'package-lock.json',
|
|
121
130
|
'*.log',
|
|
131
|
+
'*.tmp'
|
|
122
132
|
],
|
|
133
|
+
|
|
123
134
|
// File extensions to ignore
|
|
124
135
|
extensionsToIgnore: [
|
|
125
136
|
'.sqlite3',
|
|
126
137
|
'.env',
|
|
138
|
+
'.DS_Store',
|
|
139
|
+
'.ico',
|
|
140
|
+
'.png',
|
|
141
|
+
'.jpg'
|
|
127
142
|
],
|
|
128
|
-
|
|
143
|
+
|
|
144
|
+
// Directories to ignore
|
|
129
145
|
dirsToIgnore: [
|
|
130
146
|
'node_modules/',
|
|
131
147
|
'.git/',
|
|
132
148
|
'dist/',
|
|
149
|
+
'build/',
|
|
150
|
+
'coverage/'
|
|
133
151
|
],
|
|
134
|
-
|
|
152
|
+
|
|
153
|
+
// Size and performance limits
|
|
135
154
|
maxFileSize: '10MB',
|
|
155
|
+
maxTotalSize: '100MB',
|
|
156
|
+
maxDepth: 10,
|
|
157
|
+
concurrency: 10
|
|
136
158
|
};
|
|
137
159
|
```
|
|
138
160
|
|
|
139
|
-
##
|
|
161
|
+
## 📖 Command Reference
|
|
162
|
+
|
|
163
|
+
### Snapshot Command
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
eck-snapshot [options] [path]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Core Options:**
|
|
170
|
+
- `-o, --output <dir>` - Output directory (default: ./snapshots)
|
|
171
|
+
- `-d, --dir` - Directory mode: scan any folder recursively
|
|
172
|
+
- `--no-ai-header` - Skip AI instruction header (clean mode)
|
|
173
|
+
- `-v, --verbose` - Show detailed processing information
|
|
174
|
+
|
|
175
|
+
**Format & Compression:**
|
|
176
|
+
- `--format <type>` - Output format: md (default) or json
|
|
177
|
+
- `--compress` - Create gzipped output (.gz extension)
|
|
178
|
+
- `--no-tree` - Exclude directory tree from output
|
|
179
|
+
|
|
180
|
+
**Filtering:**
|
|
181
|
+
- `--include-hidden` - Include hidden files (starting with .)
|
|
182
|
+
- `--max-file-size <size>` - Maximum individual file size (e.g., 5MB)
|
|
183
|
+
- `--max-total-size <size>` - Maximum total snapshot size (e.g., 50MB)
|
|
184
|
+
- `--config <path>` - Path to custom configuration file
|
|
185
|
+
|
|
186
|
+
### Restore Command
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
eck-snapshot restore [options] <snapshot_file> [target_directory]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Control Options:**
|
|
193
|
+
- `-f, --force` - Skip confirmation prompts
|
|
194
|
+
- `--dry-run` - Preview without writing files
|
|
195
|
+
- `-v, --verbose` - Show detailed processing information
|
|
196
|
+
|
|
197
|
+
**Filtering:**
|
|
198
|
+
- `--include <patterns>` - Include only matching files (wildcards supported)
|
|
199
|
+
- `--exclude <patterns>` - Exclude matching files (wildcards supported)
|
|
200
|
+
- `--concurrency <number>` - Number of concurrent operations (default: 10)
|
|
201
|
+
|
|
202
|
+
## 🎭 Working with AI Models
|
|
203
|
+
|
|
204
|
+
### For Gemini 2.0 Pro (1M context)
|
|
205
|
+
```bash
|
|
206
|
+
# Create comprehensive snapshot with AI instructions
|
|
207
|
+
eck-snapshot --format md --compress
|
|
208
|
+
```
|
|
209
|
+
The generated file includes detailed instructions for Gemini to analyze your project and provide structured commands for Claude Code.
|
|
210
|
+
|
|
211
|
+
### For Claude Code
|
|
212
|
+
```bash
|
|
213
|
+
# Clean, focused snapshot
|
|
214
|
+
eck-snapshot --no-ai-header --max-total-size 200MB
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### For ChatGPT/Other Models
|
|
218
|
+
```bash
|
|
219
|
+
# Standard snapshot with moderate size limits
|
|
220
|
+
eck-snapshot --max-total-size 50MB --no-ai-header
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 🔧 Advanced Use Cases
|
|
224
|
+
|
|
225
|
+
### Monorepo Support
|
|
226
|
+
```bash
|
|
227
|
+
# Snapshot specific package in monorepo
|
|
228
|
+
eck-snapshot ./packages/core --dir --output ./snapshots/core
|
|
229
|
+
|
|
230
|
+
# Multiple packages
|
|
231
|
+
eck-snapshot ./packages/api --dir && eck-snapshot ./packages/web --dir
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### CI/CD Integration
|
|
235
|
+
```bash
|
|
236
|
+
# Create release snapshot
|
|
237
|
+
eck-snapshot --format json --compress --output ./artifacts
|
|
238
|
+
|
|
239
|
+
# Documentation generation
|
|
240
|
+
eck-snapshot --no-ai-header --format md --output ./docs/snapshots
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Migration & Archival
|
|
244
|
+
```bash
|
|
245
|
+
# Complete project archive
|
|
246
|
+
eck-snapshot --include-hidden --format json --compress --max-total-size 1GB
|
|
247
|
+
|
|
248
|
+
# Selective migration
|
|
249
|
+
eck-snapshot restore archive.json.gz --include "src/*" "docs/*" --exclude "*.test.*"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 📊 Output Formats
|
|
253
|
+
|
|
254
|
+
### Markdown Format (Default)
|
|
255
|
+
- Human-readable structure
|
|
256
|
+
- AI instruction headers (optional)
|
|
257
|
+
- Directory tree visualization
|
|
258
|
+
- File content with clear delimiters
|
|
259
|
+
|
|
260
|
+
### JSON Format
|
|
261
|
+
- Structured metadata
|
|
262
|
+
- Programmatic processing friendly
|
|
263
|
+
- Includes statistics and file information
|
|
264
|
+
- Perfect for automation workflows
|
|
265
|
+
|
|
266
|
+
## 🛡️ Security Features
|
|
267
|
+
|
|
268
|
+
- **Path Validation**: Prevents directory traversal during restore
|
|
269
|
+
- **File Sanitization**: Validates all file paths and names
|
|
270
|
+
- **Confirmation Prompts**: Requires approval before overwriting files
|
|
271
|
+
- **Size Limits**: Protects against extremely large operations
|
|
272
|
+
|
|
273
|
+
## 🚀 Performance
|
|
274
|
+
|
|
275
|
+
- **Parallel Processing**: Concurrent file operations for speed
|
|
276
|
+
- **Progress Tracking**: Real-time progress bars for long operations
|
|
277
|
+
- **Memory Efficient**: Streams large files to avoid memory issues
|
|
278
|
+
- **Smart Caching**: Optimized for repeated operations
|
|
279
|
+
|
|
280
|
+
## 📝 License
|
|
281
|
+
|
|
282
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
283
|
+
|
|
284
|
+
## 🤝 Contributing
|
|
285
|
+
|
|
286
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
287
|
+
|
|
288
|
+
## 📞 Support
|
|
140
289
|
|
|
141
|
-
|
|
290
|
+
- **Issues**: [GitHub Issues](https://github.com/xelth-com/eckSnapshot/issues)
|
|
291
|
+
- **Documentation**: This README and `--help` commands
|
|
292
|
+
- **Examples**: See the examples directory in the repository
|
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
|
|
3
5
|
import { Command } from 'commander';
|
|
4
6
|
import { execa } from 'execa';
|
|
5
7
|
import fs from 'fs/promises';
|
|
@@ -28,8 +30,157 @@ const DEFAULT_CONFIG = {
|
|
|
28
30
|
concurrency: 10
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generates the detailed, universal Markdown header for the snapshot file.
|
|
37
|
+
* This header contains the full operational instructions for the AI model.
|
|
38
|
+
* @param {object} stats - The statistics object from the snapshot process.
|
|
39
|
+
* @param {string} repoName - The name of the repository.
|
|
40
|
+
* @returns {string} A formatted Markdown string with comprehensive AI instructions.
|
|
41
|
+
*/
|
|
42
|
+
function generateSnapshotHeader(stats, repoName, includeAiInstructions = true) {
|
|
43
|
+
const timestamp = new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
if (!includeAiInstructions) {
|
|
46
|
+
return `# Repository Snapshot
|
|
47
|
+
|
|
48
|
+
**Repository:** ${repoName}
|
|
49
|
+
**Generated:** ${timestamp}
|
|
50
|
+
**Tool:** eck-snapshot
|
|
51
|
+
**Files Included:** ${stats.includedFiles} of ${stats.totalFiles}
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `# AI Instructions
|
|
59
|
+
|
|
60
|
+
## 1. How to Read This Snapshot
|
|
61
|
+
|
|
62
|
+
This document is a self-contained, single-file snapshot of the **${repoName}** software repository, generated by the \`eck-snapshot\` tool on **${timestamp}**. It is designed to provide a Large Language Model (LLM) with the complete context of a project.
|
|
63
|
+
|
|
64
|
+
* **Source of Truth:** Treat this snapshot as the complete and authoritative source code.
|
|
65
|
+
* **Structure:** The file contains a **Directory Structure** tree, followed by the full content of each file, demarcated by \`--- File: /path/to/file ---\` headers.
|
|
66
|
+
|
|
67
|
+
**Snapshot Stats:**
|
|
68
|
+
- **Files Included:** ${stats.includedFiles}
|
|
69
|
+
- **Total Files in Repo:** ${stats.totalFiles}
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. Your Core Operational Workflow
|
|
74
|
+
|
|
75
|
+
You are the Project Manager and Solution Architect AI. Your primary goal is to translate user requests into technical plans and then generate precise commands for a code-execution AI agent.
|
|
76
|
+
|
|
77
|
+
### PROJECT OVERVIEW
|
|
78
|
+
- **Project:** ${repoName}
|
|
79
|
+
- **Description:** A CLI tool to create and restore single-file text snapshots of a Git repository, optimized for providing context to Large Language Models (LLMs).
|
|
80
|
+
|
|
81
|
+
### CORE WORKFLOW: The Interactive Command Cycle
|
|
82
|
+
1. **Analyze User Request:** Understand the user's goal in their native language.
|
|
83
|
+
2. **Formulate a Plan:** Create a high-level technical plan to solve the user's request.
|
|
84
|
+
3. **Propose & Await Confirmation:** Present the plan to the user in their language and ask for approval to generate the command. **CRITICAL: Stop and wait for the user's response. Do NOT generate the command block at this stage.**
|
|
85
|
+
4. **Generate Command on Demand:** This is the execution step, triggered ONLY by a positive user response.
|
|
86
|
+
- **On Approval:** If the user confirms the plan (e.g., "yes", "proceed") or provides a minor correction, your *next response* must be **only the command block**. Do not include any conversational text.
|
|
87
|
+
- **On Direct Order:** If the user explicitly asks for the command (e.g., "make the command for Claude now") and you have all the necessary information, you may skip step 3 and directly generate the command block.
|
|
88
|
+
5. **Review & Report:** After the command is executed, analyze the results and report back to the user in their language.
|
|
89
|
+
6. **Iterate:** Continue the cycle based on user feedback.
|
|
90
|
+
|
|
91
|
+
### COMMUNICATION PROTOCOL
|
|
92
|
+
- **User Interaction:** ALWAYS communicate with the user in the language they use.
|
|
93
|
+
- **Agent Commands:** ALWAYS formulate the JSON payload and technical instructions for the execution agent in **ENGLISH** to ensure technical accuracy.
|
|
94
|
+
|
|
95
|
+
### COMMAND BLOCK FORMAT
|
|
96
|
+
To ensure error-free execution, all tasks for the agent must be presented in a special block with a "Copy" button. Use this enhanced format for maximum clarity and execution accuracy:
|
|
97
|
+
|
|
98
|
+
\`\`\`json
|
|
99
|
+
{
|
|
100
|
+
"command_for_agent": "apply_code_changes",
|
|
101
|
+
"task_id": "unique-task-id",
|
|
102
|
+
"payload": {
|
|
103
|
+
"objective": "Brief, clear task description",
|
|
104
|
+
"context": "Why this change is needed",
|
|
105
|
+
"files_to_modify": [
|
|
106
|
+
{
|
|
107
|
+
"path": "exact/file/path.js",
|
|
108
|
+
"action": "specific action (add, modify, replace, delete)",
|
|
109
|
+
"location": "line numbers, function name, or search pattern",
|
|
110
|
+
"details": "precise description of the change"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"new_files": [
|
|
114
|
+
{
|
|
115
|
+
"path": "path/to/new/file.js",
|
|
116
|
+
"content_type": "javascript/json/markdown/config",
|
|
117
|
+
"purpose": "why this file is needed"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"dependencies": {
|
|
121
|
+
"install": ["package-name@version"],
|
|
122
|
+
"remove": ["old-package-name"]
|
|
123
|
+
},
|
|
124
|
+
"validation_steps": [
|
|
125
|
+
"npm run test",
|
|
126
|
+
"node index.js --help",
|
|
127
|
+
"specific command to verify functionality"
|
|
128
|
+
],
|
|
129
|
+
"expected_outcome": "what should work after changes"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
### PROJECT CONTEXT (\`eck-snapshot\`)
|
|
135
|
+
- **Type:** Node.js CLI Application, executed directly.
|
|
136
|
+
- **Module System:** ES Modules (\`"type": "module"\` in package.json).
|
|
137
|
+
- **Main File:** \`index.js\` contains all primary logic (837 lines).
|
|
138
|
+
- **Configuration:** \`.ecksnapshot.config.js\` is used for custom filtering and settings.
|
|
139
|
+
- **Key Dependencies:** \`commander\`, \`execa\`, \`inquirer\`, \`ignore\`, \`p-limit\`, \`cli-progress\`.
|
|
140
|
+
|
|
141
|
+
### ARCHITECTURE DETAILS FOR CLAUDE CODE
|
|
142
|
+
**Core Functions Location:**
|
|
143
|
+
- \`createRepoSnapshot()\` - Line 333: Main snapshot creation
|
|
144
|
+
- \`restoreSnapshot()\` - Line 579: Snapshot restoration
|
|
145
|
+
- \`processFile()\` - Line 265: Individual file processing
|
|
146
|
+
- \`generateDirectoryTree()\` - Line 224: Tree generation
|
|
147
|
+
- \`generateSnapshotHeader()\` - Line 42: AI instruction header
|
|
148
|
+
- CLI setup - Lines 800-837: Commander.js configuration
|
|
149
|
+
|
|
150
|
+
**Common Modification Patterns:**
|
|
151
|
+
- CLI options: Modify commander setup (lines 808-822, 824-835)
|
|
152
|
+
- Configuration: Update DEFAULT_CONFIG object (lines 23-31)
|
|
153
|
+
- File processing: Enhance processFile() function
|
|
154
|
+
- Output formats: Modify generateSnapshotHeader() or output logic
|
|
155
|
+
- Dependencies: Update package.json and import statements
|
|
156
|
+
|
|
157
|
+
**Testing Status:**
|
|
158
|
+
- No test framework currently configured
|
|
159
|
+
- package.json test script returns error
|
|
160
|
+
- Manual testing via \`node index.js\` commands
|
|
161
|
+
- Consider adding vitest or jest for future testing
|
|
162
|
+
|
|
163
|
+
**Development Workflow:**
|
|
164
|
+
- Direct execution: \`node index.js [command] [options]\`
|
|
165
|
+
- Package creation: \`npm pack\`
|
|
166
|
+
- Local testing: \`node index.js --help\`
|
|
167
|
+
- Configuration testing: modify \`.ecksnapshot.config.js\`
|
|
168
|
+
|
|
169
|
+
**Critical Implementation Notes:**
|
|
170
|
+
- All file paths normalized to forward slashes
|
|
171
|
+
- ES module imports only (no CommonJS)
|
|
172
|
+
- Error handling with detailed user messages
|
|
173
|
+
- Progress tracking for long operations
|
|
174
|
+
- Security: Path validation prevents directory traversal
|
|
175
|
+
- Cross-platform compatibility maintained
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
33
184
|
|
|
34
185
|
function parseSize(sizeStr) {
|
|
35
186
|
const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
|
|
@@ -67,7 +218,6 @@ function matchesPattern(filePath, patterns) {
|
|
|
67
218
|
|
|
68
219
|
async function loadConfig(configPath) {
|
|
69
220
|
let config = { ...DEFAULT_CONFIG };
|
|
70
|
-
|
|
71
221
|
if (configPath) {
|
|
72
222
|
try {
|
|
73
223
|
const configModule = await import(path.resolve(configPath));
|
|
@@ -82,7 +232,6 @@ async function loadConfig(configPath) {
|
|
|
82
232
|
'.ecksnapshot.config.mjs',
|
|
83
233
|
'ecksnapshot.config.js'
|
|
84
234
|
];
|
|
85
|
-
|
|
86
235
|
for (const configFile of possibleConfigs) {
|
|
87
236
|
try {
|
|
88
237
|
await fs.access(configFile);
|
|
@@ -110,11 +259,55 @@ async function checkGitAvailability() {
|
|
|
110
259
|
async function checkGitRepository(repoPath) {
|
|
111
260
|
try {
|
|
112
261
|
await execa('git', ['rev-parse', '--git-dir'], { cwd: repoPath });
|
|
262
|
+
return true;
|
|
113
263
|
} catch (error) {
|
|
114
|
-
|
|
264
|
+
return false;
|
|
115
265
|
}
|
|
116
266
|
}
|
|
117
267
|
|
|
268
|
+
async function scanDirectoryRecursively(dirPath, config, relativeTo = dirPath) {
|
|
269
|
+
const files = [];
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
273
|
+
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
276
|
+
const relativePath = path.relative(relativeTo, fullPath).replace(/\\/g, '/');
|
|
277
|
+
|
|
278
|
+
// Skip if matches ignore patterns
|
|
279
|
+
if (config.dirsToIgnore.some(dir =>
|
|
280
|
+
entry.name === dir.replace('/', '') ||
|
|
281
|
+
relativePath.startsWith(dir)
|
|
282
|
+
)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Skip hidden files unless explicitly included
|
|
287
|
+
if (!config.includeHidden && entry.name.startsWith('.')) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (entry.isDirectory()) {
|
|
292
|
+
const subFiles = await scanDirectoryRecursively(fullPath, config, relativeTo);
|
|
293
|
+
files.push(...subFiles);
|
|
294
|
+
} else {
|
|
295
|
+
// Skip ignored files and extensions
|
|
296
|
+
if (config.extensionsToIgnore.includes(path.extname(entry.name)) ||
|
|
297
|
+
matchesPattern(relativePath, config.filesToIgnore)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
files.push(relativePath);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn(`⚠️ Warning: Could not read directory: ${dirPath} - ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return files;
|
|
309
|
+
}
|
|
310
|
+
|
|
118
311
|
async function loadGitignore(repoPath) {
|
|
119
312
|
try {
|
|
120
313
|
const gitignoreContent = await fs.readFile(path.join(repoPath, '.gitignore'), 'utf-8');
|
|
@@ -142,7 +335,6 @@ async function readFileWithSizeCheck(filePath, maxFileSize) {
|
|
|
142
335
|
|
|
143
336
|
async function generateDirectoryTree(dir, prefix = '', allFiles, depth = 0, maxDepth = 10, config) {
|
|
144
337
|
if (depth > maxDepth) return '';
|
|
145
|
-
|
|
146
338
|
try {
|
|
147
339
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
148
340
|
const sortedEntries = entries.sort((a, b) => {
|
|
@@ -150,16 +342,13 @@ async function generateDirectoryTree(dir, prefix = '', allFiles, depth = 0, maxD
|
|
|
150
342
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
151
343
|
return a.name.localeCompare(b.name);
|
|
152
344
|
});
|
|
153
|
-
|
|
154
345
|
let tree = '';
|
|
155
346
|
const validEntries = [];
|
|
156
347
|
|
|
157
348
|
for (const entry of sortedEntries) {
|
|
158
349
|
if (config.dirsToIgnore.some(d => entry.name.includes(d.replace('/', '')))) continue;
|
|
159
|
-
|
|
160
350
|
const fullPath = path.join(dir, entry.name);
|
|
161
351
|
const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
162
|
-
|
|
163
352
|
if (entry.isDirectory() || allFiles.includes(relativePath)) {
|
|
164
353
|
validEntries.push({ entry, fullPath, relativePath });
|
|
165
354
|
}
|
|
@@ -171,7 +360,6 @@ async function generateDirectoryTree(dir, prefix = '', allFiles, depth = 0, maxD
|
|
|
171
360
|
|
|
172
361
|
const connector = isLast ? '└── ' : '├── ';
|
|
173
362
|
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
174
|
-
|
|
175
363
|
if (entry.isDirectory()) {
|
|
176
364
|
tree += `${prefix}${connector}${entry.name}/\n`;
|
|
177
365
|
tree += await generateDirectoryTree(fullPath, nextPrefix, allFiles, depth + 1, maxDepth, config);
|
|
@@ -227,10 +415,10 @@ async function processFile(filePath, config, gitignore, stats) {
|
|
|
227
415
|
|
|
228
416
|
stats.includedFiles++;
|
|
229
417
|
stats.includedFileTypes.set(fileExt, (stats.includedFileTypes.get(fileExt) || 0) + 1);
|
|
230
|
-
|
|
231
418
|
return { content: fileContent, size: fileContent.length };
|
|
232
419
|
} catch (error) {
|
|
233
|
-
const errorReason = error.message.includes('too large') ?
|
|
420
|
+
const errorReason = error.message.includes('too large') ?
|
|
421
|
+
'file-too-large' : 'read-error';
|
|
234
422
|
|
|
235
423
|
stats.errors.push({ file: filePath, error: error.message });
|
|
236
424
|
stats.skippedFiles++;
|
|
@@ -255,12 +443,11 @@ async function processFile(filePath, config, gitignore, stats) {
|
|
|
255
443
|
// --- ОСНОВНЫЕ ФУНКЦИИ ДЛЯ КОМАНД ---
|
|
256
444
|
|
|
257
445
|
async function createRepoSnapshot(repoPath, options) {
|
|
258
|
-
// ... (эта функция остается без изменений)
|
|
259
446
|
const absoluteRepoPath = path.resolve(repoPath);
|
|
260
447
|
const absoluteOutputPath = path.resolve(options.output);
|
|
261
448
|
const originalCwd = process.cwd();
|
|
262
449
|
|
|
263
|
-
console.log(`🚀 Starting snapshot for repository: ${absoluteRepoPath}`);
|
|
450
|
+
console.log(`🚀 Starting snapshot for ${options.dir ? 'directory' : 'repository'}: ${absoluteRepoPath}`);
|
|
264
451
|
console.log(`📁 Snapshots will be saved to: ${absoluteOutputPath}`);
|
|
265
452
|
|
|
266
453
|
try {
|
|
@@ -269,19 +456,37 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
269
456
|
config.maxTotalSize = options.maxTotalSize || config.maxTotalSize;
|
|
270
457
|
config.maxDepth = options.maxDepth || config.maxDepth;
|
|
271
458
|
config.includeHidden = options.includeHidden || false;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
459
|
+
|
|
460
|
+
let allFiles = [];
|
|
461
|
+
let gitignore = null;
|
|
462
|
+
let isGitRepo = false;
|
|
463
|
+
|
|
464
|
+
// Check if it's a git repository (unless --dir is explicitly used)
|
|
465
|
+
if (!options.dir) {
|
|
466
|
+
await checkGitAvailability();
|
|
467
|
+
isGitRepo = await checkGitRepository(absoluteRepoPath);
|
|
468
|
+
|
|
469
|
+
if (!isGitRepo) {
|
|
470
|
+
console.log('ℹ️ Not a git repository, switching to directory mode');
|
|
471
|
+
options.dir = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
275
474
|
|
|
276
475
|
process.chdir(absoluteRepoPath);
|
|
277
476
|
console.log('✅ Successfully changed working directory');
|
|
278
477
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
478
|
+
if (options.dir) {
|
|
479
|
+
console.log('📋 Scanning directory recursively...');
|
|
480
|
+
allFiles = await scanDirectoryRecursively(absoluteRepoPath, config);
|
|
481
|
+
gitignore = ignore(); // Empty gitignore for directory mode
|
|
482
|
+
console.log(`📊 Found ${allFiles.length} total files in the directory`);
|
|
483
|
+
} else {
|
|
484
|
+
gitignore = await loadGitignore(absoluteRepoPath);
|
|
485
|
+
console.log('📋 Fetching file list from Git...');
|
|
486
|
+
const { stdout } = await execa('git', ['ls-files']);
|
|
487
|
+
allFiles = stdout.split('\n').filter(Boolean);
|
|
488
|
+
console.log(`📊 Found ${allFiles.length} total files in the repository`);
|
|
489
|
+
}
|
|
285
490
|
|
|
286
491
|
const stats = {
|
|
287
492
|
totalFiles: allFiles.length,
|
|
@@ -295,7 +500,6 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
295
500
|
skipReasons: new Map(),
|
|
296
501
|
skippedFilesDetails: new Map()
|
|
297
502
|
};
|
|
298
|
-
|
|
299
503
|
let snapshotContent = '';
|
|
300
504
|
|
|
301
505
|
if (options.tree) {
|
|
@@ -308,13 +512,13 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
308
512
|
|
|
309
513
|
console.log('📝 Processing files...');
|
|
310
514
|
const limit = pLimit(config.concurrency);
|
|
311
|
-
const progressBar = options.verbose ?
|
|
515
|
+
const progressBar = options.verbose ?
|
|
516
|
+
null : new SingleBar({
|
|
312
517
|
format: 'Progress |{bar}| {percentage}% | {value}/{total} files | ETA: {eta}s',
|
|
313
518
|
barCompleteChar: '\u2588',
|
|
314
519
|
barIncompleteChar: '\u2591',
|
|
315
520
|
hideCursor: true
|
|
316
521
|
}, Presets.shades_classic);
|
|
317
|
-
|
|
318
522
|
if (progressBar) progressBar.start(allFiles.length, 0);
|
|
319
523
|
|
|
320
524
|
const filePromises = allFiles.map((filePath, index) =>
|
|
@@ -325,6 +529,7 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
325
529
|
progressBar.update(index + 1);
|
|
326
530
|
} else if (options.verbose) {
|
|
327
531
|
if (result.skipped) {
|
|
532
|
+
|
|
328
533
|
console.log(`⏭️ Skipping: ${filePath} (${result.reason})`);
|
|
329
534
|
} else {
|
|
330
535
|
console.log(`✅ Processed: ${filePath}`);
|
|
@@ -334,14 +539,12 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
334
539
|
return result;
|
|
335
540
|
})
|
|
336
541
|
);
|
|
337
|
-
|
|
338
542
|
const results = await Promise.allSettled(filePromises);
|
|
339
543
|
if (progressBar) progressBar.stop();
|
|
340
544
|
|
|
341
545
|
const contentArray = [];
|
|
342
546
|
let totalSize = 0;
|
|
343
547
|
const maxTotalSize = parseSize(config.maxTotalSize);
|
|
344
|
-
|
|
345
548
|
for (const result of results) {
|
|
346
549
|
if (result.status === 'rejected') {
|
|
347
550
|
console.warn(`⚠️ Promise rejected: ${result.reason}`);
|
|
@@ -357,21 +560,22 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
357
560
|
}
|
|
358
561
|
}
|
|
359
562
|
|
|
360
|
-
|
|
563
|
+
// Add the header to the beginning of the file content
|
|
564
|
+
const repoName = path.basename(absoluteRepoPath);
|
|
565
|
+
const header = generateSnapshotHeader(stats, repoName, options.aiHeader !== false);
|
|
566
|
+
snapshotContent = header + snapshotContent + contentArray.join('');
|
|
567
|
+
|
|
361
568
|
const totalChars = snapshotContent.length;
|
|
362
569
|
const estimatedTokens = Math.round(totalChars / 4);
|
|
363
570
|
|
|
364
571
|
const timestamp = new Date().toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-');
|
|
365
|
-
const
|
|
366
|
-
const extension = options.format === 'json' ? 'json' : 'txt';
|
|
572
|
+
const extension = options.format === 'json' ? 'json' : 'md'; // Changed default to md
|
|
367
573
|
let outputFilename = `${repoName}_snapshot_${timestamp}.${extension}`;
|
|
368
|
-
|
|
369
574
|
if (options.compress) {
|
|
370
575
|
outputFilename += '.gz';
|
|
371
576
|
}
|
|
372
577
|
|
|
373
578
|
const fullOutputFilePath = path.join(absoluteOutputPath, outputFilename);
|
|
374
|
-
|
|
375
579
|
let finalContent = snapshotContent;
|
|
376
580
|
if (options.format === 'json') {
|
|
377
581
|
const jsonData = {
|
|
@@ -383,6 +587,7 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
383
587
|
skippedFileTypes: Object.fromEntries(stats.skippedFileTypes),
|
|
384
588
|
skipReasons: Object.fromEntries(stats.skipReasons),
|
|
385
589
|
skippedFilesDetails: Object.fromEntries(
|
|
590
|
+
|
|
386
591
|
Array.from(stats.skippedFilesDetails.entries()).map(([reason, files]) => [
|
|
387
592
|
reason,
|
|
388
593
|
files.map(({file, ext}) => ({file, ext}))
|
|
@@ -395,7 +600,6 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
395
600
|
}
|
|
396
601
|
|
|
397
602
|
await fs.mkdir(absoluteOutputPath, { recursive: true });
|
|
398
|
-
|
|
399
603
|
if (options.compress) {
|
|
400
604
|
const compressed = await gzip(finalContent);
|
|
401
605
|
await fs.writeFile(fullOutputFilePath, compressed);
|
|
@@ -421,8 +625,7 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
421
625
|
console.log('\n📋 Included File Types Distribution:');
|
|
422
626
|
const sortedIncludedTypes = Array.from(stats.includedFileTypes.entries())
|
|
423
627
|
.sort(([,a], [,b]) => b - a)
|
|
424
|
-
.slice(0, 10);
|
|
425
|
-
|
|
628
|
+
.slice(0, 10);
|
|
426
629
|
for (const [ext, count] of sortedIncludedTypes) {
|
|
427
630
|
console.log(` ${ext}: ${count} files`);
|
|
428
631
|
}
|
|
@@ -432,8 +635,7 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
432
635
|
console.log('\n⏭️ Skipped File Types Distribution:');
|
|
433
636
|
const sortedSkippedTypes = Array.from(stats.skippedFileTypes.entries())
|
|
434
637
|
.sort(([,a], [,b]) => b - a)
|
|
435
|
-
.slice(0, 10);
|
|
436
|
-
|
|
638
|
+
.slice(0, 10);
|
|
437
639
|
for (const [ext, count] of sortedSkippedTypes) {
|
|
438
640
|
console.log(` ${ext}: ${count} files`);
|
|
439
641
|
}
|
|
@@ -443,7 +645,6 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
443
645
|
console.log('\n📊 Skip Reasons:');
|
|
444
646
|
const sortedReasons = Array.from(stats.skipReasons.entries())
|
|
445
647
|
.sort(([,a], [,b]) => b - a);
|
|
446
|
-
|
|
447
648
|
const reasonLabels = {
|
|
448
649
|
'ignored-directory': 'Ignored directories',
|
|
449
650
|
'ignored-extension': 'Ignored extensions',
|
|
@@ -454,7 +655,6 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
454
655
|
'file-too-large': 'Files too large',
|
|
455
656
|
'read-error': 'Read errors'
|
|
456
657
|
};
|
|
457
|
-
|
|
458
658
|
for (const [reason, count] of sortedReasons) {
|
|
459
659
|
const label = reasonLabels[reason] || reason;
|
|
460
660
|
console.log(` ${label}: ${count} files`);
|
|
@@ -471,7 +671,7 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
471
671
|
console.log(` ... and ${files.length - 10} more files`);
|
|
472
672
|
}
|
|
473
673
|
}
|
|
474
|
-
console.log();
|
|
674
|
+
console.log();
|
|
475
675
|
}
|
|
476
676
|
}
|
|
477
677
|
|
|
@@ -486,7 +686,6 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
486
686
|
}
|
|
487
687
|
|
|
488
688
|
console.log('='.repeat(50));
|
|
489
|
-
|
|
490
689
|
} catch (error) {
|
|
491
690
|
console.error('\n❌ An error occurred:');
|
|
492
691
|
if (error.code === 'ENOENT' && error.path && error.path.includes('.git')) {
|
|
@@ -509,115 +708,170 @@ async function createRepoSnapshot(repoPath, options) {
|
|
|
509
708
|
}
|
|
510
709
|
}
|
|
511
710
|
|
|
512
|
-
// NEW: Restore Snapshot Function
|
|
513
711
|
async function restoreSnapshot(snapshotFile, targetDir, options) {
|
|
514
712
|
const absoluteSnapshotPath = path.resolve(snapshotFile);
|
|
515
713
|
const absoluteTargetDir = path.resolve(targetDir);
|
|
516
|
-
|
|
517
|
-
console.log(
|
|
518
|
-
console.log(`🗂️ Target directory: ${absoluteTargetDir}`);
|
|
714
|
+
console.log(`🔄 Starting restore from snapshot: ${absoluteSnapshotPath}`);
|
|
715
|
+
console.log(`📁 Target directory: ${absoluteTargetDir}`);
|
|
519
716
|
|
|
520
717
|
try {
|
|
521
718
|
let rawContent;
|
|
522
|
-
// Check if the file is compressed
|
|
523
719
|
if (snapshotFile.endsWith('.gz')) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
720
|
+
const compressedBuffer = await fs.readFile(absoluteSnapshotPath);
|
|
721
|
+
rawContent = (await gunzip(compressedBuffer)).toString('utf-8');
|
|
722
|
+
console.log('✅ Decompressed gzipped snapshot');
|
|
527
723
|
} else {
|
|
528
|
-
|
|
724
|
+
rawContent = await fs.readFile(absoluteSnapshotPath, 'utf-8');
|
|
529
725
|
}
|
|
530
726
|
|
|
531
|
-
// Check if the content is JSON
|
|
532
727
|
let filesToRestore;
|
|
533
728
|
try {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
729
|
+
const jsonData = JSON.parse(rawContent);
|
|
730
|
+
if (jsonData.content) {
|
|
731
|
+
console.log('📄 Detected JSON format, extracting content');
|
|
732
|
+
filesToRestore = parseSnapshotContent(jsonData.content);
|
|
733
|
+
} else {
|
|
734
|
+
throw new Error('JSON format detected, but no "content" key found');
|
|
735
|
+
}
|
|
541
736
|
} catch (e) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
filesToRestore = parseSnapshotContent(rawContent);
|
|
737
|
+
console.log('📄 Treating snapshot as plain text format');
|
|
738
|
+
filesToRestore = parseSnapshotContent(rawContent);
|
|
545
739
|
}
|
|
546
740
|
|
|
547
741
|
if (filesToRestore.length === 0) {
|
|
548
|
-
console.warn('⚠️ No files found to restore in the snapshot
|
|
742
|
+
console.warn('⚠️ No files found to restore in the snapshot');
|
|
549
743
|
return;
|
|
550
744
|
}
|
|
551
745
|
|
|
552
|
-
|
|
746
|
+
// Apply filters if specified
|
|
747
|
+
if (options.include || options.exclude) {
|
|
748
|
+
filesToRestore = filterFilesToRestore(filesToRestore, options);
|
|
749
|
+
if (filesToRestore.length === 0) {
|
|
750
|
+
console.warn('⚠️ No files remaining after applying filters');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
553
754
|
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
default: false,
|
|
562
|
-
},
|
|
563
|
-
]);
|
|
755
|
+
// Validate file paths for security
|
|
756
|
+
const invalidFiles = validateFilePaths(filesToRestore, absoluteTargetDir);
|
|
757
|
+
if (invalidFiles.length > 0) {
|
|
758
|
+
console.error('❌ Invalid file paths detected (potential directory traversal):');
|
|
759
|
+
invalidFiles.forEach(file => console.error(` ${file}`));
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
564
762
|
|
|
763
|
+
console.log(`📊 Found ${filesToRestore.length} files to restore`);
|
|
764
|
+
if (options.dryRun) {
|
|
765
|
+
console.log('\n🔍 Dry run mode - files that would be restored:');
|
|
766
|
+
filesToRestore.forEach(file => {
|
|
767
|
+
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
768
|
+
console.log(` ${fullPath}`);
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (!options.force) {
|
|
774
|
+
const { confirm } = await inquirer.prompt([{
|
|
775
|
+
type: 'confirm',
|
|
776
|
+
name: 'confirm',
|
|
777
|
+
message: `You are about to write ${filesToRestore.length} files to ${absoluteTargetDir}. Existing files will be overwritten. Continue?`,
|
|
778
|
+
default: false
|
|
779
|
+
}]);
|
|
565
780
|
if (!confirm) {
|
|
566
|
-
console.log('🚫 Restore operation cancelled by user
|
|
781
|
+
console.log('🚫 Restore operation cancelled by user');
|
|
567
782
|
return;
|
|
568
783
|
}
|
|
569
784
|
}
|
|
570
785
|
|
|
571
|
-
// Create target directory if it doesn't exist
|
|
572
786
|
await fs.mkdir(absoluteTargetDir, { recursive: true });
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
787
|
+
const stats = {
|
|
788
|
+
totalFiles: filesToRestore.length,
|
|
789
|
+
restoredFiles: 0,
|
|
790
|
+
failedFiles: 0,
|
|
791
|
+
errors: []
|
|
792
|
+
};
|
|
793
|
+
const progressBar = options.verbose ? null : new SingleBar({
|
|
794
|
+
format: 'Restoring |{bar}| {percentage}% | {value}/{total} files',
|
|
795
|
+
barCompleteChar: '\u2588',
|
|
796
|
+
barIncompleteChar: '\u2591',
|
|
797
|
+
hideCursor: true
|
|
580
798
|
}, Presets.shades_classic);
|
|
799
|
+
if (progressBar) progressBar.start(filesToRestore.length, 0);
|
|
581
800
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
801
|
+
const limit = pLimit(options.concurrency || 10);
|
|
802
|
+
const filePromises = filesToRestore.map((file, index) =>
|
|
803
|
+
limit(async () => {
|
|
804
|
+
try {
|
|
805
|
+
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
806
|
+
const dir = path.dirname(fullPath);
|
|
587
807
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
808
|
+
await fs.mkdir(dir, { recursive: true });
|
|
809
|
+
await fs.writeFile(fullPath, file.content, 'utf-8');
|
|
810
|
+
|
|
811
|
+
stats.restoredFiles++;
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
if (progressBar) {
|
|
815
|
+
progressBar.update(index + 1);
|
|
816
|
+
} else if (options.verbose) {
|
|
817
|
+
console.log(`✅ Restored: ${file.path}`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return { success: true, file: file.path };
|
|
821
|
+
|
|
822
|
+
} catch (error) {
|
|
823
|
+
stats.failedFiles++;
|
|
824
|
+
stats.errors.push({ file: file.path, error: error.message });
|
|
825
|
+
|
|
826
|
+
if (options.verbose) {
|
|
827
|
+
console.log(`❌ Failed to restore: ${file.path} - ${error.message}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
return { success: false, file: file.path, error: error.message };
|
|
832
|
+
}
|
|
833
|
+
})
|
|
834
|
+
);
|
|
594
835
|
|
|
595
|
-
|
|
596
|
-
|
|
836
|
+
await Promise.allSettled(filePromises);
|
|
837
|
+
if (progressBar) progressBar.stop();
|
|
597
838
|
|
|
839
|
+
console.log('\n📊 Restore Summary');
|
|
840
|
+
console.log('='.repeat(50));
|
|
841
|
+
console.log(`🎉 Restore completed!`);
|
|
842
|
+
console.log(`✅ Successfully restored: ${stats.restoredFiles} files`);
|
|
843
|
+
if (stats.failedFiles > 0) {
|
|
844
|
+
console.log(`❌ Failed to restore: ${stats.failedFiles} files`);
|
|
845
|
+
if (stats.errors.length > 0) {
|
|
846
|
+
console.log('\n⚠️ Errors encountered:');
|
|
847
|
+
stats.errors.slice(0, 5).forEach(({ file, error }) => {
|
|
848
|
+
console.log(` ${file}: ${error}`);
|
|
849
|
+
});
|
|
850
|
+
if (stats.errors.length > 5) {
|
|
851
|
+
console.log(` ... and ${stats.errors.length - 5} more errors`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
console.log(`📁 Target directory: ${absoluteTargetDir}`);
|
|
856
|
+
console.log('='.repeat(50));
|
|
598
857
|
} catch (error) {
|
|
599
858
|
console.error('\n❌ An error occurred during restore:');
|
|
600
859
|
console.error(error.message);
|
|
601
860
|
if (options.verbose) {
|
|
602
|
-
|
|
861
|
+
console.error(error.stack);
|
|
603
862
|
}
|
|
604
863
|
process.exit(1);
|
|
605
864
|
}
|
|
606
865
|
}
|
|
607
866
|
|
|
608
|
-
// NEW: Snapshot Parser Function
|
|
609
867
|
function parseSnapshotContent(content) {
|
|
610
868
|
const files = [];
|
|
611
869
|
const fileRegex = /--- File: \/(.+) ---/g;
|
|
612
870
|
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
871
|
for (let i = 1; i < sections.length; i += 2) {
|
|
617
872
|
const filePath = sections[i].trim();
|
|
618
873
|
let fileContent = sections[i + 1] || '';
|
|
619
874
|
|
|
620
|
-
// Remove the trailing newline that the split might leave
|
|
621
875
|
if (fileContent.startsWith('\n\n')) {
|
|
622
876
|
fileContent = fileContent.substring(2);
|
|
623
877
|
}
|
|
@@ -631,6 +885,48 @@ function parseSnapshotContent(content) {
|
|
|
631
885
|
return files;
|
|
632
886
|
}
|
|
633
887
|
|
|
888
|
+
function filterFilesToRestore(files, options) {
|
|
889
|
+
let filtered = files;
|
|
890
|
+
|
|
891
|
+
if (options.include) {
|
|
892
|
+
const includePatterns = Array.isArray(options.include) ?
|
|
893
|
+
options.include : [options.include];
|
|
894
|
+
filtered = filtered.filter(file =>
|
|
895
|
+
includePatterns.some(pattern => {
|
|
896
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
897
|
+
return regex.test(file.path);
|
|
898
|
+
})
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (options.exclude) {
|
|
903
|
+
const excludePatterns = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
|
|
904
|
+
filtered = filtered.filter(file =>
|
|
905
|
+
!excludePatterns.some(pattern => {
|
|
906
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
907
|
+
return regex.test(file.path);
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return filtered;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function validateFilePaths(files, targetDir) {
|
|
916
|
+
const invalidFiles = [];
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
const normalizedPath = path.normalize(file.path);
|
|
919
|
+
if (normalizedPath.includes('..') ||
|
|
920
|
+
normalizedPath.startsWith('/') ||
|
|
921
|
+
normalizedPath.includes('\0') ||
|
|
922
|
+
/[<>:"|?*]/.test(normalizedPath)) {
|
|
923
|
+
invalidFiles.push(file.path);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return invalidFiles;
|
|
928
|
+
}
|
|
929
|
+
|
|
634
930
|
|
|
635
931
|
// --- CLI SETUP ---
|
|
636
932
|
const program = new Command();
|
|
@@ -638,14 +934,14 @@ const program = new Command();
|
|
|
638
934
|
program
|
|
639
935
|
.name('eck-snapshot')
|
|
640
936
|
.description('A CLI tool to create and restore single-file text snapshots of a Git repository.')
|
|
641
|
-
.version('
|
|
937
|
+
.version('3.0.0');
|
|
642
938
|
|
|
643
939
|
// Snapshot command (existing)
|
|
644
940
|
program
|
|
645
941
|
.command('snapshot', { isDefault: true })
|
|
646
942
|
.description('Create a snapshot of a Git repository (default command).')
|
|
647
943
|
.argument('[repoPath]', 'Path to the git repository to snapshot.', process.cwd())
|
|
648
|
-
|
|
944
|
+
.option('-o, --output <dir>', 'Output directory for the snapshot file.', path.join(__dirname, 'snapshots'))
|
|
649
945
|
.option('--no-tree', 'Do not include the directory tree in the snapshot.')
|
|
650
946
|
.option('-v, --verbose', 'Show detailed processing information, including skipped files.')
|
|
651
947
|
.option('--max-file-size <size>', 'Maximum file size to include (e.g., 10MB)', '10MB')
|
|
@@ -654,17 +950,22 @@ program
|
|
|
654
950
|
.option('--config <path>', 'Path to configuration file')
|
|
655
951
|
.option('--compress', 'Compress output file with gzip')
|
|
656
952
|
.option('--include-hidden', 'Include hidden files (starting with .)')
|
|
657
|
-
.option('--format <type>', 'Output format:
|
|
953
|
+
.option('--format <type>', 'Output format: md, json', 'md')
|
|
954
|
+
.option('--no-ai-header', 'Skip AI instruction header (create clean snapshot)')
|
|
955
|
+
.option('-d, --dir', 'Directory mode: scan directory recursively (auto-enabled if no git repo found)')
|
|
658
956
|
.action((repoPath, options) => createRepoSnapshot(repoPath, options));
|
|
659
957
|
|
|
660
|
-
// NEW: Restore command (Corrected)
|
|
661
958
|
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
|
|
666
|
-
.option('-f, --force', 'Force overwrite of existing files without confirmation
|
|
667
|
-
.option('-v, --verbose', 'Show detailed processing information
|
|
959
|
+
.command('restore')
|
|
960
|
+
.description('Restore files and directories from a snapshot file')
|
|
961
|
+
.argument('<snapshot_file>', 'Path to the snapshot file (.txt, .json, or .gz)')
|
|
962
|
+
.argument('[target_directory]', 'Directory to restore the files into', process.cwd())
|
|
963
|
+
.option('-f, --force', 'Force overwrite of existing files without confirmation')
|
|
964
|
+
.option('-v, --verbose', 'Show detailed processing information')
|
|
965
|
+
.option('--dry-run', 'Show what would be restored without actually writing files')
|
|
966
|
+
.option('--include <patterns...>', 'Include only files matching these patterns (supports wildcards)')
|
|
967
|
+
.option('--exclude <patterns...>', 'Exclude files matching these patterns (supports wildcards)')
|
|
968
|
+
.option('--concurrency <number>', 'Number of concurrent file operations', (val) => parseInt(val), 10)
|
|
668
969
|
.action((snapshotFile, targetDir, options) => restoreSnapshot(snapshotFile, targetDir, options));
|
|
669
970
|
|
|
670
971
|
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 and restore single-file text snapshots of
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "A powerful CLI tool to create and restore single-file text snapshots of Git repositories and directories. Optimized for AI context and LLM workflows.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -31,4 +31,4 @@
|
|
|
31
31
|
"p-limit": "^5.0.0",
|
|
32
32
|
"inquirer": "^9.2.20"
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|