snow-ai 0.2.5 → 0.2.7

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.
@@ -0,0 +1,299 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ /**
6
+ * Workspace Snapshot Manager
7
+ * Provides git-like version control for workspace files
8
+ */
9
+ class WorkspaceSnapshotManager {
10
+ constructor() {
11
+ Object.defineProperty(this, "snapshotsDir", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
17
+ Object.defineProperty(this, "activeSnapshot", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: null
22
+ });
23
+ Object.defineProperty(this, "ignorePatterns", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: [
28
+ 'node_modules/**',
29
+ '.git/**',
30
+ 'dist/**',
31
+ 'build/**',
32
+ '.snow/**',
33
+ '*.log',
34
+ '.DS_Store',
35
+ '*.swp',
36
+ '*.swo',
37
+ '*~',
38
+ '.vscode/**',
39
+ '.idea/**'
40
+ ]
41
+ });
42
+ this.snapshotsDir = path.join(os.homedir(), '.snow', 'snapshots');
43
+ }
44
+ /**
45
+ * Ensure snapshots directory exists
46
+ */
47
+ async ensureSnapshotsDir() {
48
+ await fs.mkdir(this.snapshotsDir, { recursive: true });
49
+ }
50
+ /**
51
+ * Calculate SHA256 hash of file content
52
+ */
53
+ async calculateFileHash(filePath) {
54
+ const content = await fs.readFile(filePath);
55
+ return crypto.createHash('sha256').update(content).digest('hex');
56
+ }
57
+ /**
58
+ * Recursively scan directory and collect files
59
+ */
60
+ async scanDirectory(dirPath, basePath, fileStates) {
61
+ try {
62
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ // Skip ignored patterns
65
+ if (this.ignorePatterns.some(pattern => {
66
+ const normalized = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
67
+ return entry.name.match(new RegExp(normalized));
68
+ })) {
69
+ continue;
70
+ }
71
+ const fullPath = path.join(dirPath, entry.name);
72
+ const relativePath = path.relative(basePath, fullPath);
73
+ if (entry.isDirectory()) {
74
+ // Recursively scan subdirectory
75
+ await this.scanDirectory(fullPath, basePath, fileStates);
76
+ }
77
+ else if (entry.isFile()) {
78
+ try {
79
+ const stats = await fs.stat(fullPath);
80
+ const hash = await this.calculateFileHash(fullPath);
81
+ fileStates.set(relativePath, {
82
+ path: relativePath,
83
+ hash,
84
+ size: stats.size,
85
+ mtime: stats.mtimeMs
86
+ });
87
+ }
88
+ catch (error) {
89
+ // Skip files that can't be read
90
+ console.error(`Failed to process file ${relativePath}:`, error);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error(`Failed to scan directory ${dirPath}:`, error);
97
+ }
98
+ }
99
+ /**
100
+ * Scan workspace and build file state map
101
+ */
102
+ async scanWorkspace(workspaceRoot) {
103
+ const fileStates = new Map();
104
+ await this.scanDirectory(workspaceRoot, workspaceRoot, fileStates);
105
+ return fileStates;
106
+ }
107
+ /**
108
+ * Create a snapshot of current workspace state
109
+ */
110
+ async createSnapshot(sessionId, messageCount, workspaceRoot = process.cwd()) {
111
+ await this.ensureSnapshotsDir();
112
+ const files = await this.scanWorkspace(workspaceRoot);
113
+ this.activeSnapshot = {
114
+ sessionId,
115
+ messageCount,
116
+ timestamp: Date.now(),
117
+ workspaceRoot,
118
+ files
119
+ };
120
+ // Note: We don't save the snapshot to disk yet
121
+ // We'll only save changed files when rollback is triggered
122
+ }
123
+ /**
124
+ * Get snapshot metadata path
125
+ */
126
+ getSnapshotPath(sessionId) {
127
+ return path.join(this.snapshotsDir, `${sessionId}.json`);
128
+ }
129
+ /**
130
+ * Get snapshot content directory
131
+ */
132
+ getSnapshotContentDir(sessionId) {
133
+ return path.join(this.snapshotsDir, sessionId);
134
+ }
135
+ /**
136
+ * Create snapshot when user sends message
137
+ */
138
+ async createSnapshotForMessage(sessionId, messageIndex, workspaceRoot = process.cwd()) {
139
+ await this.ensureSnapshotsDir();
140
+ const snapshotContentDir = this.getSnapshotContentDir(`${sessionId}_${messageIndex}`);
141
+ await fs.mkdir(snapshotContentDir, { recursive: true });
142
+ // Scan current workspace state
143
+ const files = await this.scanWorkspace(workspaceRoot);
144
+ // Save file contents to snapshot directory
145
+ const fileBackups = [];
146
+ for (const [relativePath, fileState] of files) {
147
+ const fullPath = path.join(workspaceRoot, relativePath);
148
+ const backupPath = path.join(snapshotContentDir, relativePath);
149
+ try {
150
+ // Create directory structure
151
+ await fs.mkdir(path.dirname(backupPath), { recursive: true });
152
+ // Copy file to snapshot directory
153
+ await fs.copyFile(fullPath, backupPath);
154
+ fileBackups.push({
155
+ path: relativePath,
156
+ hash: fileState.hash
157
+ });
158
+ }
159
+ catch (error) {
160
+ console.error(`Failed to backup file ${relativePath}:`, error);
161
+ }
162
+ }
163
+ // Save snapshot metadata
164
+ const metadata = {
165
+ sessionId: `${sessionId}_${messageIndex}`,
166
+ messageCount: messageIndex,
167
+ timestamp: Date.now(),
168
+ workspaceRoot,
169
+ changedFiles: fileBackups.map(f => ({
170
+ path: f.path,
171
+ content: null, // Content stored separately in snapshot directory
172
+ existed: true
173
+ }))
174
+ };
175
+ const metadataPath = this.getSnapshotPath(`${sessionId}_${messageIndex}`);
176
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
177
+ }
178
+ /**
179
+ * List all snapshots for a session
180
+ */
181
+ async listSnapshots(sessionId) {
182
+ await this.ensureSnapshotsDir();
183
+ const snapshots = [];
184
+ try {
185
+ const files = await fs.readdir(this.snapshotsDir);
186
+ for (const file of files) {
187
+ if (file.startsWith(sessionId) && file.endsWith('.json')) {
188
+ const metadataPath = path.join(this.snapshotsDir, file);
189
+ const content = await fs.readFile(metadataPath, 'utf-8');
190
+ const metadata = JSON.parse(content);
191
+ snapshots.push({
192
+ messageIndex: metadata.messageCount,
193
+ timestamp: metadata.timestamp
194
+ });
195
+ }
196
+ }
197
+ }
198
+ catch (error) {
199
+ console.error('Failed to list snapshots:', error);
200
+ }
201
+ return snapshots.sort((a, b) => b.messageIndex - a.messageIndex);
202
+ }
203
+ /**
204
+ * Rollback workspace to specific snapshot
205
+ */
206
+ async rollbackToSnapshot(sessionId, messageIndex) {
207
+ const snapshotId = `${sessionId}_${messageIndex}`;
208
+ const metadataPath = this.getSnapshotPath(snapshotId);
209
+ const snapshotContentDir = this.getSnapshotContentDir(snapshotId);
210
+ try {
211
+ // Load snapshot metadata
212
+ const metadataContent = await fs.readFile(metadataPath, 'utf-8');
213
+ const metadata = JSON.parse(metadataContent);
214
+ // Get current workspace files
215
+ const currentFiles = await this.scanWorkspace(metadata.workspaceRoot);
216
+ // Find files that exist now but didn't exist in snapshot
217
+ // (these should be deleted)
218
+ const snapshotFilePaths = new Set(metadata.changedFiles.map(f => f.path));
219
+ for (const [relativePath] of currentFiles) {
220
+ if (!snapshotFilePaths.has(relativePath)) {
221
+ // This file was created after snapshot, delete it
222
+ const fullPath = path.join(metadata.workspaceRoot, relativePath);
223
+ try {
224
+ await fs.unlink(fullPath);
225
+ }
226
+ catch (error) {
227
+ console.error(`Failed to delete new file ${relativePath}:`, error);
228
+ }
229
+ }
230
+ }
231
+ // Restore all files from snapshot
232
+ for (const fileBackup of metadata.changedFiles) {
233
+ const backupPath = path.join(snapshotContentDir, fileBackup.path);
234
+ const fullPath = path.join(metadata.workspaceRoot, fileBackup.path);
235
+ try {
236
+ // Restore file from backup
237
+ await fs.copyFile(backupPath, fullPath);
238
+ }
239
+ catch (error) {
240
+ console.error(`Failed to restore file ${fileBackup.path}:`, error);
241
+ }
242
+ }
243
+ // Don't clean up snapshot - keep for future rollbacks
244
+ return metadata.messageCount;
245
+ }
246
+ catch (error) {
247
+ console.error('Failed to rollback workspace:', error);
248
+ return null;
249
+ }
250
+ }
251
+ /**
252
+ * Clear snapshot for a session
253
+ */
254
+ async clearSnapshot(sessionId) {
255
+ const metadataPath = this.getSnapshotPath(sessionId);
256
+ const snapshotContentDir = this.getSnapshotContentDir(sessionId);
257
+ try {
258
+ // Delete metadata
259
+ await fs.unlink(metadataPath);
260
+ }
261
+ catch (error) {
262
+ // Ignore if doesn't exist
263
+ }
264
+ try {
265
+ // Delete snapshot content directory
266
+ await fs.rm(snapshotContentDir, { recursive: true, force: true });
267
+ }
268
+ catch (error) {
269
+ // Ignore if doesn't exist
270
+ }
271
+ if (this.activeSnapshot?.sessionId === sessionId) {
272
+ this.activeSnapshot = null;
273
+ }
274
+ }
275
+ /**
276
+ * Clear all snapshots for a session
277
+ */
278
+ async clearAllSnapshots(sessionId) {
279
+ await this.ensureSnapshotsDir();
280
+ try {
281
+ const files = await fs.readdir(this.snapshotsDir);
282
+ for (const file of files) {
283
+ if (file.startsWith(sessionId)) {
284
+ const filePath = path.join(this.snapshotsDir, file);
285
+ if (file.endsWith('.json')) {
286
+ await fs.unlink(filePath);
287
+ }
288
+ else {
289
+ await fs.rm(filePath, { recursive: true, force: true });
290
+ }
291
+ }
292
+ }
293
+ }
294
+ catch (error) {
295
+ console.error('Failed to clear snapshots:', error);
296
+ }
297
+ }
298
+ }
299
+ export const workspaceSnapshotManager = new WorkspaceSnapshotManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -63,6 +63,7 @@
63
63
  "@sindresorhus/tsconfig": "^3.0.1",
64
64
  "@types/diff": "^7.0.2",
65
65
  "@types/figlet": "^1.7.0",
66
+ "@types/glob": "^8.1.0",
66
67
  "@types/react": "^18.0.32",
67
68
  "@types/ws": "^8.5.8",
68
69
  "@vdemedes/prettier-config": "^2.0.1",