@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -80
  3. package/index.js +413 -112
  4. 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://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
+ [![npm version](https://badge.fury.io/js/%40xelth%2Feck-snapshot.svg)](https://www.npmjs.com/package/@xelth/eck-snapshot)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/xelth-com/eckSnapshot/blob/main/LICENSE)
5
5
 
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).
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 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.
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
- ## Key Features
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
- * **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
+ ## 🚀 Key Features
21
22
 
22
- ## Demo
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
- Here's an example of `eck-snapshot` in action:
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
- 🚀 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
- ```
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
- The beginning of the generated file will look like this:
45
+ ## 📦 Installation
46
+
47
+ ```bash
48
+ npm install -g @xelth/eck-snapshot
49
+ ```
45
50
 
46
- ```text
47
- Directory Structure:
51
+ ## 🎯 Quick Start
48
52
 
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
53
+ ### Create Snapshots
59
54
 
55
+ ```bash
56
+ # Git repository (default mode)
57
+ eck-snapshot
60
58
 
61
- --- File: /src/index.js ---
59
+ # Any directory (auto-detects non-git folders)
60
+ eck-snapshot /path/to/any/folder
62
61
 
63
- #!/usr/bin/env node
64
- import { Command } from 'commander';
65
- // ... rest of the file content
62
+ # Force directory mode (ignores git)
63
+ eck-snapshot --dir .
66
64
 
67
- --- File: /package.json ---
65
+ # Clean snapshot without AI instructions
66
+ eck-snapshot --no-ai-header
68
67
 
69
- {
70
- "name": "eck-snapshot",
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
- ## Installation
76
-
77
- To install the tool globally, run the following command:
72
+ ### Restore from Snapshots
78
73
 
79
74
  ```bash
80
- npm install -g @xelth/eck-snapshot
81
- ```
75
+ # Basic restore
76
+ eck-snapshot restore snapshot.md
82
77
 
83
- ## Usage
78
+ # Restore to specific directory
79
+ eck-snapshot restore snapshot.md ./restored-project
84
80
 
85
- Once installed, you can run the tool from any directory in your terminal.
81
+ # Preview without writing files
82
+ eck-snapshot restore snapshot.md --dry-run
86
83
 
87
- ### Creating a Snapshot
84
+ # Restore only specific files
85
+ eck-snapshot restore snapshot.md --include "*.js" "*.json"
86
+ ```
88
87
 
89
- ```bash
90
- # Create a snapshot of the current directory
91
- eck-snapshot
88
+ ## 📋 Usage Examples
89
+
90
+ ### For AI Development
92
91
 
93
- # Specify a path to another repository
94
- eck-snapshot /path/to/your/other/project
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
- # Save the snapshot to a different directory and exclude the tree view
97
- eck-snapshot --output ./backups --no-tree
97
+ # Clean snapshot for general documentation
98
+ eck-snapshot --no-ai-header --output ./docs
98
99
  ```
99
100
 
100
- ### Restoring from a Snapshot
101
+ ### Project Backup & Migration
101
102
 
102
103
  ```bash
103
- # Restore files from a snapshot into the current directory
104
- eck-snapshot restore ./snapshots/project_snapshot_...txt
104
+ # Full project backup
105
+ eck-snapshot --include-hidden --format json --compress
105
106
 
106
- # Restore into a new directory without a confirmation prompt
107
- eck-snapshot restore snapshot.txt ./restored-project --force
107
+ # Selective restore
108
+ eck-snapshot restore backup.json.gz --exclude "node_modules/*" --include "src/*"
108
109
  ```
109
110
 
110
- ## Configuration
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
- You can create a `.ecksnapshot.config.js` file in your project's root directory to customize the tool's behavior.
121
+ ## ⚙️ Configuration
113
122
 
114
- **Example `.ecksnapshot.config.js`:**
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
- // Directories to ignore (must have a trailing slash)
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
- // Maximum size for individual files
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
- ## License
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
- This project is licensed under the MIT License.
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
- // ... (остальные существующие функции: parseSize, formatSize, matchesPattern, loadConfig, и т.д. остаются без изменений)
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
- throw new Error(`Not a git repository: ${repoPath}`);
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') ? 'file-too-large' : 'read-error';
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
- await checkGitAvailability();
274
- await checkGitRepository(absoluteRepoPath);
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
- const gitignore = await loadGitignore(absoluteRepoPath);
280
-
281
- console.log('📋 Fetching file list from Git...');
282
- const { stdout } = await execa('git', ['ls-files']);
283
- const allFiles = stdout.split('\n').filter(Boolean);
284
- console.log(`📊 Found ${allFiles.length} total files in the repository`);
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 ? null : new SingleBar({
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
- snapshotContent += contentArray.join('');
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 repoName = path.basename(absoluteRepoPath);
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(`⚙️ Starting restore from snapshot: ${absoluteSnapshotPath}`);
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
- const compressedBuffer = await fs.readFile(absoluteSnapshotPath);
525
- rawContent = (await gunzip(compressedBuffer)).toString('utf-8');
526
- console.log('✅ Decompressed gzipped snapshot.');
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
- rawContent = await fs.readFile(absoluteSnapshotPath, 'utf-8');
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
- 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
- }
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
- // 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);
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
- console.log(`📊 Found ${filesToRestore.length} files in the snapshot.`);
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
- // 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
- ]);
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
- // Restore files
575
- const progressBar = new SingleBar({
576
- format: 'Restoring |{bar}| {percentage}% | {value}/{total} files',
577
- barCompleteChar: '\u2588',
578
- barIncompleteChar: '\u2591',
579
- hideCursor: true
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
- 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);
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
- // 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
- }
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
- progressBar.stop();
596
- console.log(`\n🎉 Restore complete! ${filesToRestore.length} files have been written to ${absoluteTargetDir}.`);
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
- console.error(error.stack);
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('2.1.0');
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
- .option('-o, --output <dir>', 'Output directory for the snapshot file.', path.join(process.cwd(), 'snapshots'))
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: txt, json', 'txt')
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.', process.cwd())
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": "2.1.0",
4
- "description": "A CLI tool to create and restore single-file text snapshots of a Git repository.",
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
+ }